Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
426973e3c4 | ||
|
|
52e0de022f | ||
|
|
eb56bc8b88 | ||
|
|
9f90678151 | ||
|
|
22f0be4d04 | ||
|
|
19bc0b65d9 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,22 @@
|
||||
## [2.18.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.17.3...v2.18.0) (2025-10-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add centralized player ownership utilities module ([52e0de0](https://github.com/antialias/soroban-abacus-flashcards/commit/52e0de022fcd332fc4cfffa7bfcfe6adc69cb3ff))
|
||||
|
||||
## [2.17.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.17.2...v2.17.3) (2025-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correct build-info.json import path in type declaration ([22f0be4](https://github.com/antialias/soroban-abacus-flashcards/commit/22f0be4d045c6afdcb98b876f709d63a904f9449))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add plan for centralizing player ownership logic ([d3ff89a](https://github.com/antialias/soroban-abacus-flashcards/commit/d3ff89a0ee53c32cc68ed01bf460919aa889d6a0))
|
||||
|
||||
## [2.17.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.17.1...v2.17.2) (2025-10-10)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
"Bash(git stash:*)",
|
||||
"Bash(npm run format:*)",
|
||||
"Bash(npm run pre-commit:*)",
|
||||
"Bash(npm run type-check:*)"
|
||||
"Bash(npm run type-check:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(npx @biomejs/biome format:*)",
|
||||
"Bash(npx @biomejs/biome check:*)",
|
||||
"Bash(npm run lint:fix:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -68,7 +68,10 @@ function localMemoryPairsReducer(state: MemoryPairsState, action: LocalAction):
|
||||
matchedPairs: 0,
|
||||
moves: 0,
|
||||
scores: action.activePlayers.reduce((acc: any, p: string) => ({ ...acc, [p]: 0 }), {}),
|
||||
consecutiveMatches: action.activePlayers.reduce((acc: any, p: string) => ({ ...acc, [p]: 0 }), {}),
|
||||
consecutiveMatches: action.activePlayers.reduce(
|
||||
(acc: any, p: string) => ({ ...acc, [p]: 0 }),
|
||||
{}
|
||||
),
|
||||
activePlayers: action.activePlayers,
|
||||
playerMetadata: action.playerMetadata,
|
||||
currentPlayer: action.activePlayers[0] || '',
|
||||
@@ -97,7 +100,8 @@ function localMemoryPairsReducer(state: MemoryPairsState, action: LocalAction):
|
||||
return {
|
||||
...state,
|
||||
flippedCards: newFlippedCards,
|
||||
currentMoveStartTime: state.flippedCards.length === 0 ? Date.now() : state.currentMoveStartTime,
|
||||
currentMoveStartTime:
|
||||
state.flippedCards.length === 0 ? Date.now() : state.currentMoveStartTime,
|
||||
isProcessingMove: newFlippedCards.length === 2,
|
||||
showMismatchFeedback: false,
|
||||
}
|
||||
@@ -386,7 +390,14 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
|
||||
|
||||
return true
|
||||
},
|
||||
[isGameActive, state.isProcessingMove, state.gameCards, state.flippedCards, state.currentPlayer, players]
|
||||
[
|
||||
isGameActive,
|
||||
state.isProcessingMove,
|
||||
state.gameCards,
|
||||
state.flippedCards,
|
||||
state.currentPlayer,
|
||||
players,
|
||||
]
|
||||
)
|
||||
|
||||
const currentGameStatistics: GameStatistics = useMemo(
|
||||
|
||||
258
apps/web/src/lib/arcade/__tests__/player-ownership.test.ts
Normal file
258
apps/web/src/lib/arcade/__tests__/player-ownership.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Unit tests for player ownership utilities
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RoomData } from '@/hooks/useRoomData'
|
||||
import {
|
||||
type PlayerOwnershipMap,
|
||||
buildPlayerMetadata,
|
||||
buildPlayerOwnershipFromRoomData,
|
||||
getPlayerOwner,
|
||||
isPlayerOwnedByUser,
|
||||
isUsersTurn,
|
||||
} from '../player-ownership'
|
||||
|
||||
describe('player-ownership utilities', () => {
|
||||
describe('buildPlayerOwnershipFromRoomData', () => {
|
||||
it('builds ownership map from roomData.memberPlayers', () => {
|
||||
const roomData: RoomData = {
|
||||
id: 'room-1',
|
||||
name: 'Test Room',
|
||||
code: 'ABC123',
|
||||
gameName: 'matching',
|
||||
members: [],
|
||||
memberPlayers: {
|
||||
'user-1': [
|
||||
{ id: 'player-1', name: 'Player 1', emoji: '😀', color: '#3b82f6' },
|
||||
{ id: 'player-2', name: 'Player 2', emoji: '😎', color: '#8b5cf6' },
|
||||
],
|
||||
'user-2': [{ id: 'player-3', name: 'Player 3', emoji: '🤠', color: '#10b981' }],
|
||||
},
|
||||
}
|
||||
|
||||
const ownershipMap = buildPlayerOwnershipFromRoomData(roomData)
|
||||
|
||||
expect(ownershipMap).toEqual({
|
||||
'player-1': 'user-1',
|
||||
'player-2': 'user-1',
|
||||
'player-3': 'user-2',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns empty object for null roomData', () => {
|
||||
const ownershipMap = buildPlayerOwnershipFromRoomData(null)
|
||||
expect(ownershipMap).toEqual({})
|
||||
})
|
||||
|
||||
it('returns empty object for undefined roomData', () => {
|
||||
const ownershipMap = buildPlayerOwnershipFromRoomData(undefined)
|
||||
expect(ownershipMap).toEqual({})
|
||||
})
|
||||
|
||||
it('returns empty object for roomData without memberPlayers', () => {
|
||||
const roomData = {
|
||||
id: 'room-1',
|
||||
name: 'Test Room',
|
||||
code: 'ABC123',
|
||||
gameName: 'matching',
|
||||
members: [],
|
||||
memberPlayers: {},
|
||||
} as RoomData
|
||||
|
||||
const ownershipMap = buildPlayerOwnershipFromRoomData(roomData)
|
||||
expect(ownershipMap).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPlayerOwnedByUser', () => {
|
||||
const ownershipMap: PlayerOwnershipMap = {
|
||||
'player-1': 'user-1',
|
||||
'player-2': 'user-1',
|
||||
'player-3': 'user-2',
|
||||
}
|
||||
|
||||
it('returns true when player is owned by user', () => {
|
||||
expect(isPlayerOwnedByUser('player-1', 'user-1', ownershipMap)).toBe(true)
|
||||
expect(isPlayerOwnedByUser('player-2', 'user-1', ownershipMap)).toBe(true)
|
||||
expect(isPlayerOwnedByUser('player-3', 'user-2', ownershipMap)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when player is not owned by user', () => {
|
||||
expect(isPlayerOwnedByUser('player-1', 'user-2', ownershipMap)).toBe(false)
|
||||
expect(isPlayerOwnedByUser('player-3', 'user-1', ownershipMap)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for unknown player', () => {
|
||||
expect(isPlayerOwnedByUser('player-unknown', 'user-1', ownershipMap)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPlayerOwner', () => {
|
||||
const ownershipMap: PlayerOwnershipMap = {
|
||||
'player-1': 'user-1',
|
||||
'player-2': 'user-1',
|
||||
'player-3': 'user-2',
|
||||
}
|
||||
|
||||
it('returns correct owner userId for player', () => {
|
||||
expect(getPlayerOwner('player-1', ownershipMap)).toBe('user-1')
|
||||
expect(getPlayerOwner('player-2', ownershipMap)).toBe('user-1')
|
||||
expect(getPlayerOwner('player-3', ownershipMap)).toBe('user-2')
|
||||
})
|
||||
|
||||
it('returns undefined for unknown player', () => {
|
||||
expect(getPlayerOwner('player-unknown', ownershipMap)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUsersTurn', () => {
|
||||
const ownershipMap: PlayerOwnershipMap = {
|
||||
'player-1': 'user-1',
|
||||
'player-2': 'user-1',
|
||||
'player-3': 'user-2',
|
||||
}
|
||||
|
||||
it('returns true when current player belongs to user', () => {
|
||||
expect(isUsersTurn('player-1', 'user-1', ownershipMap)).toBe(true)
|
||||
expect(isUsersTurn('player-3', 'user-2', ownershipMap)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when current player belongs to different user', () => {
|
||||
expect(isUsersTurn('player-1', 'user-2', ownershipMap)).toBe(false)
|
||||
expect(isUsersTurn('player-3', 'user-1', ownershipMap)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for unknown player', () => {
|
||||
expect(isUsersTurn('player-unknown', 'user-1', ownershipMap)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildPlayerMetadata', () => {
|
||||
const ownershipMap: PlayerOwnershipMap = {
|
||||
'player-1': 'user-1',
|
||||
'player-2': 'user-1',
|
||||
'player-3': 'user-2',
|
||||
}
|
||||
|
||||
const playersMap = new Map([
|
||||
['player-1', { name: 'Player 1', emoji: '😀', color: '#3b82f6' }],
|
||||
['player-2', { name: 'Player 2', emoji: '😎', color: '#8b5cf6' }],
|
||||
['player-3', { name: 'Player 3', emoji: '🤠', color: '#10b981' }],
|
||||
])
|
||||
|
||||
it('builds metadata with correct ownership', () => {
|
||||
const metadata = buildPlayerMetadata(
|
||||
['player-1', 'player-2', 'player-3'],
|
||||
ownershipMap,
|
||||
playersMap
|
||||
)
|
||||
|
||||
expect(metadata).toEqual({
|
||||
'player-1': {
|
||||
id: 'player-1',
|
||||
name: 'Player 1',
|
||||
emoji: '😀',
|
||||
userId: 'user-1',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
'player-2': {
|
||||
id: 'player-2',
|
||||
name: 'Player 2',
|
||||
emoji: '😎',
|
||||
userId: 'user-1',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
'player-3': {
|
||||
id: 'player-3',
|
||||
name: 'Player 3',
|
||||
emoji: '🤠',
|
||||
userId: 'user-2',
|
||||
color: '#10b981',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('uses fallback userId when player not in ownership map', () => {
|
||||
const metadata = buildPlayerMetadata(
|
||||
['player-1', 'player-4'],
|
||||
ownershipMap,
|
||||
playersMap,
|
||||
'fallback-user'
|
||||
)
|
||||
|
||||
// player-1 has ownership, but player-4 is not in playersMap
|
||||
// so it won't be in metadata at all
|
||||
expect(metadata['player-1']?.userId).toBe('user-1')
|
||||
expect(metadata['player-4']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips players not in playersMap', () => {
|
||||
const metadata = buildPlayerMetadata(['player-1', 'player-unknown'], ownershipMap, playersMap)
|
||||
|
||||
expect(metadata['player-1']).toBeDefined()
|
||||
expect(metadata['player-unknown']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles empty playerIds array', () => {
|
||||
const metadata = buildPlayerMetadata([], ownershipMap, playersMap)
|
||||
expect(metadata).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty ownership map', () => {
|
||||
const emptyMap: PlayerOwnershipMap = {}
|
||||
|
||||
expect(isPlayerOwnedByUser('player-1', 'user-1', emptyMap)).toBe(false)
|
||||
expect(getPlayerOwner('player-1', emptyMap)).toBeUndefined()
|
||||
expect(isUsersTurn('player-1', 'user-1', emptyMap)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles empty strings', () => {
|
||||
const ownershipMap: PlayerOwnershipMap = {
|
||||
'player-1': 'user-1',
|
||||
}
|
||||
|
||||
expect(isPlayerOwnedByUser('', 'user-1', ownershipMap)).toBe(false)
|
||||
expect(getPlayerOwner('', ownershipMap)).toBeUndefined()
|
||||
expect(isUsersTurn('', 'user-1', ownershipMap)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('real-world scenario: turn indicator logic', () => {
|
||||
it('reproduces the "Your turn" vs "Their turn" bug and fix', () => {
|
||||
const roomData: RoomData = {
|
||||
id: 'room-1',
|
||||
name: 'Game Room',
|
||||
code: 'ABC123',
|
||||
gameName: 'matching',
|
||||
members: [],
|
||||
memberPlayers: {
|
||||
'local-user-id': [
|
||||
{ id: 'local-player-1', name: 'My Player 1', emoji: '😀', color: '#3b82f6' },
|
||||
{ id: 'local-player-2', name: 'My Player 2', emoji: '😎', color: '#8b5cf6' },
|
||||
],
|
||||
'remote-user-id': [
|
||||
{ id: 'remote-player-1', name: 'Their Player', emoji: '🤠', color: '#10b981' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const ownershipMap = buildPlayerOwnershipFromRoomData(roomData)
|
||||
const viewerId = 'local-user-id'
|
||||
|
||||
// Scenario 1: It's my turn (local player is current)
|
||||
const currentPlayer1 = 'local-player-1'
|
||||
const isMyTurn1 = isUsersTurn(currentPlayer1, viewerId, ownershipMap)
|
||||
expect(isMyTurn1).toBe(true)
|
||||
expect(isMyTurn1 ? 'Your turn' : 'Their turn').toBe('Your turn')
|
||||
|
||||
// Scenario 2: It's their turn (remote player is current)
|
||||
const currentPlayer2 = 'remote-player-1'
|
||||
const isMyTurn2 = isUsersTurn(currentPlayer2, viewerId, ownershipMap)
|
||||
expect(isMyTurn2).toBe(false)
|
||||
expect(isMyTurn2 ? 'Your turn' : 'Their turn').toBe('Their turn')
|
||||
})
|
||||
})
|
||||
})
|
||||
225
apps/web/src/lib/arcade/player-ownership.ts
Normal file
225
apps/web/src/lib/arcade/player-ownership.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Player Ownership Utilities
|
||||
*
|
||||
* Centralized module for determining player ownership across the codebase.
|
||||
* Provides consistent utilities for both server-side (DB-based) and client-side
|
||||
* (RoomData-based) player ownership checking.
|
||||
*
|
||||
* This module solves the problem of scattered player ownership logic that
|
||||
* previously existed in 4+ locations with different implementations.
|
||||
*/
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db, schema } from '@/db'
|
||||
import type { RoomData } from '@/hooks/useRoomData'
|
||||
|
||||
/**
|
||||
* Player ownership mapping: playerId -> userId
|
||||
*
|
||||
* This is the canonical representation of player ownership used throughout
|
||||
* the application. Both server-side and client-side utilities return this type.
|
||||
*/
|
||||
export type PlayerOwnershipMap = Record<string, string>
|
||||
|
||||
/**
|
||||
* Player metadata with ownership information
|
||||
*
|
||||
* Used when building player metadata for game state that needs to be
|
||||
* shared across room members.
|
||||
*/
|
||||
export interface PlayerMetadata {
|
||||
id: string
|
||||
name: string
|
||||
emoji: string
|
||||
userId: string // Owner's user ID
|
||||
color: string
|
||||
}
|
||||
|
||||
/**
|
||||
* SERVER-SIDE: Build player ownership map from database
|
||||
*
|
||||
* Queries the database to get all players and their owner userIds.
|
||||
* Used by session-manager and validators for authorization checks.
|
||||
*
|
||||
* @param roomId - Optional room ID to filter players (currently unused, fetches all)
|
||||
* @returns Promise resolving to playerOwnership map
|
||||
*
|
||||
* @example
|
||||
* const ownership = await buildPlayerOwnershipMap()
|
||||
* // { "player-uuid-1": "user-uuid-1", "player-uuid-2": "user-uuid-2" }
|
||||
*/
|
||||
export async function buildPlayerOwnershipMap(roomId?: string): Promise<PlayerOwnershipMap> {
|
||||
// Fetch all players with their userId ownership
|
||||
const players = await db.query.players.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Convert to ownership map: playerId -> userId
|
||||
return Object.fromEntries(players.map((p) => [p.id, p.userId]))
|
||||
}
|
||||
|
||||
/**
|
||||
* CLIENT-SIDE: Build player ownership map from RoomData
|
||||
*
|
||||
* Constructs ownership map from the memberPlayers structure in RoomData.
|
||||
* Used by React components and providers for client-side ownership checks.
|
||||
*
|
||||
* @param roomData - Room data containing memberPlayers mapping
|
||||
* @returns PlayerOwnershipMap
|
||||
*
|
||||
* @example
|
||||
* const ownership = buildPlayerOwnershipFromRoomData(roomData)
|
||||
* // { "player-uuid-1": "user-uuid-1", "player-uuid-2": "user-uuid-2" }
|
||||
*/
|
||||
export function buildPlayerOwnershipFromRoomData(
|
||||
roomData: RoomData | null | undefined
|
||||
): PlayerOwnershipMap {
|
||||
if (!roomData?.memberPlayers) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const ownershipMap: PlayerOwnershipMap = {}
|
||||
|
||||
// memberPlayers is Record<userId, RoomPlayer[]>
|
||||
// We need to invert it to Record<playerId, userId>
|
||||
for (const [userId, userPlayers] of Object.entries(roomData.memberPlayers)) {
|
||||
for (const player of userPlayers) {
|
||||
ownershipMap[player.id] = userId
|
||||
}
|
||||
}
|
||||
|
||||
return ownershipMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is owned by a specific user
|
||||
*
|
||||
* @param playerId - The player ID to check
|
||||
* @param userId - The user ID to check ownership against
|
||||
* @param ownershipMap - Player ownership mapping
|
||||
* @returns true if the player belongs to the user
|
||||
*
|
||||
* @example
|
||||
* const isOwned = isPlayerOwnedByUser(playerId, currentUserId, ownershipMap)
|
||||
* if (!isOwned) {
|
||||
* return { valid: false, error: 'Not your player' }
|
||||
* }
|
||||
*/
|
||||
export function isPlayerOwnedByUser(
|
||||
playerId: string,
|
||||
userId: string,
|
||||
ownershipMap: PlayerOwnershipMap
|
||||
): boolean {
|
||||
return ownershipMap[playerId] === userId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the owner userId for a player
|
||||
*
|
||||
* @param playerId - The player ID to look up
|
||||
* @param ownershipMap - Player ownership mapping
|
||||
* @returns The owner's userId, or undefined if not found
|
||||
*
|
||||
* @example
|
||||
* const owner = getPlayerOwner(playerId, ownershipMap)
|
||||
* if (owner !== currentUserId) {
|
||||
* console.log('This player belongs to another user')
|
||||
* }
|
||||
*/
|
||||
export function getPlayerOwner(
|
||||
playerId: string,
|
||||
ownershipMap: PlayerOwnershipMap
|
||||
): string | undefined {
|
||||
return ownershipMap[playerId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Build player metadata with correct ownership information
|
||||
*
|
||||
* Combines player data with ownership information to create complete
|
||||
* metadata objects. This is used when starting games or sending player
|
||||
* info across the network.
|
||||
*
|
||||
* @param playerIds - Array of player IDs to include
|
||||
* @param ownershipMap - Player ownership mapping
|
||||
* @param playersMap - Map of player ID to player data (from GameModeContext)
|
||||
* @param fallbackUserId - UserId to use if player not found in ownership map
|
||||
* @returns Record of playerId to PlayerMetadata
|
||||
*
|
||||
* @example
|
||||
* const metadata = buildPlayerMetadata(
|
||||
* activePlayers,
|
||||
* ownershipMap,
|
||||
* players,
|
||||
* viewerId
|
||||
* )
|
||||
* // Send metadata with game state
|
||||
*/
|
||||
export function buildPlayerMetadata(
|
||||
playerIds: string[],
|
||||
ownershipMap: PlayerOwnershipMap,
|
||||
playersMap: Map<string, { name: string; emoji: string; color: string }>,
|
||||
fallbackUserId?: string
|
||||
): Record<string, PlayerMetadata> {
|
||||
const metadata: Record<string, PlayerMetadata> = {}
|
||||
|
||||
for (const playerId of playerIds) {
|
||||
const playerData = playersMap.get(playerId)
|
||||
if (!playerData) continue
|
||||
|
||||
// Get the actual owner userId from ownership map, or use fallback
|
||||
const ownerUserId = ownershipMap[playerId] || fallbackUserId || ''
|
||||
|
||||
metadata[playerId] = {
|
||||
id: playerId,
|
||||
name: playerData.name,
|
||||
emoji: playerData.emoji,
|
||||
userId: ownerUserId,
|
||||
color: playerData.color,
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a specific user's turn in a game
|
||||
*
|
||||
* Convenience function that combines current player check with ownership check.
|
||||
*
|
||||
* @param currentPlayerId - The ID of the player whose turn it is
|
||||
* @param userId - The user ID to check
|
||||
* @param ownershipMap - Player ownership mapping
|
||||
* @returns true if it's this user's turn
|
||||
*
|
||||
* @example
|
||||
* const isMyTurn = isUsersTurn(state.currentPlayer, viewerId, ownershipMap)
|
||||
* const label = isMyTurn ? 'Your turn' : 'Their turn'
|
||||
*/
|
||||
export function isUsersTurn(
|
||||
currentPlayerId: string,
|
||||
userId: string,
|
||||
ownershipMap: PlayerOwnershipMap
|
||||
): boolean {
|
||||
return isPlayerOwnedByUser(currentPlayerId, userId, ownershipMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* SERVER-SIDE: Convert guestId to internal userId
|
||||
*
|
||||
* Helper to convert the guestId (from cookies) to the internal database userId.
|
||||
* This is needed because the database uses internal user.id as foreign keys.
|
||||
*
|
||||
* @param guestId - The guest ID from the cookie
|
||||
* @returns The internal user ID, or undefined if not found
|
||||
*/
|
||||
export async function getUserIdFromGuestId(guestId: string): Promise<string | undefined> {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(schema.users.guestId, guestId),
|
||||
columns: { id: true },
|
||||
})
|
||||
return user?.id
|
||||
}
|
||||
2
apps/web/src/types/build-info.d.ts
vendored
2
apps/web/src/types/build-info.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module '@/generated/build-info.json' {
|
||||
declare module '../generated/build-info.json' {
|
||||
interface BuildInfo {
|
||||
version: string
|
||||
buildTime: string
|
||||
|
||||
@@ -24,10 +24,5 @@
|
||||
"src/app/games/matching/utils/matchValidation.ts",
|
||||
"socket-server.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
"exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "2.17.2",
|
||||
"version": "2.18.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user