diff --git a/.vscode/settings.json b/.vscode/settings.json index 68b17798..97e1e3c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,14 @@ { "files.trimTrailingWhitespace": true, "npm.packageManager": "yarn", - "cSpell.words": ["injectable", "nestjs", "postgres", "styleguide", "typeorm"], + "cSpell.words": [ + "injectable", + "memeber", + "nestjs", + "postgres", + "styleguide", + "typeorm" + ], "gitlens.advanced.blame.customArguments": [], "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" diff --git a/apps/auth-front/src/components/AvatarDropzone/AvatarDropzone.tsx b/apps/auth-front/src/components/AvatarDropzone/AvatarDropzone.tsx index d8f41c11..9f9ebc66 100644 --- a/apps/auth-front/src/components/AvatarDropzone/AvatarDropzone.tsx +++ b/apps/auth-front/src/components/AvatarDropzone/AvatarDropzone.tsx @@ -33,12 +33,23 @@ export const AvatarDropzone = memo(({ avatarUrl, onChange }: DropZoneProps) => { maxSize: MAX_FILE_SIZE, onDropAccepted: acceptedFiles => { const acceptedFile = acceptedFiles[0] + + if (!acceptedFile) { + return + } + setStateFile({ file: acceptedFile, previewUrl: URL.createObjectURL(acceptedFile) }) clearError() onChange?.(acceptedFile) }, onDropRejected: fileRejections => { - handleError(fileRejections[0]) + const fileRejection = fileRejections[0] + + if (!fileRejection) { + return + } + + handleError(fileRejection) }, }) @@ -57,7 +68,7 @@ export const AvatarDropzone = memo(({ avatarUrl, onChange }: DropZoneProps) => {
{!isDragActive ? ( -
+
{stateFile?.file ? ( ) : ( diff --git a/apps/auth-front/tsconfig.json b/apps/auth-front/tsconfig.json index 7041971b..26f523d9 100644 --- a/apps/auth-front/tsconfig.json +++ b/apps/auth-front/tsconfig.json @@ -10,6 +10,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, diff --git a/apps/blog/src/app/[locale]/feed/_utils/channelInfoCreator.ts b/apps/blog/src/app/[locale]/feed/_utils/channelInfoCreator.ts index dc5076e7..a96e2735 100644 --- a/apps/blog/src/app/[locale]/feed/_utils/channelInfoCreator.ts +++ b/apps/blog/src/app/[locale]/feed/_utils/channelInfoCreator.ts @@ -44,7 +44,7 @@ export const createChannelInfo = async (lang: Language): Promise => description: t('main.description'), atomUrl: generateFullUrl(`/feed/${lang}`), url: generateFullUrl(`/${lang}/posts`), - updatedAt: latestPost.publishedAt, + updatedAt: latestPost?.publishedAt ?? new Date().toISOString(), } return { ...channelMetadata, posts: feedPosts } diff --git a/apps/blog/src/app/[locale]/posts/_utils/filterBlogPosts.test.ts b/apps/blog/src/app/[locale]/posts/_utils/filterBlogPosts.test.ts index 3092f069..80c8afc7 100644 --- a/apps/blog/src/app/[locale]/posts/_utils/filterBlogPosts.test.ts +++ b/apps/blog/src/app/[locale]/posts/_utils/filterBlogPosts.test.ts @@ -69,8 +69,8 @@ const posts = [ }, ] -posts[0].translates = { en: posts[2] } -posts[2].translates = { ru: posts[1] } +posts[0]!.translates = { en: posts[2] } +posts[2]!.translates = { ru: posts[1] } describe('filterBlogPosts', () => { it('Should return all (uniq by locale) posts if search and tags empty', () => { diff --git a/apps/blog/src/generation-utils/mapHeadingsToTOC.js b/apps/blog/src/generation-utils/mapHeadingsToTOC.js index a68a6af5..e311c2e7 100644 --- a/apps/blog/src/generation-utils/mapHeadingsToTOC.js +++ b/apps/blog/src/generation-utils/mapHeadingsToTOC.js @@ -19,6 +19,7 @@ export function mapHeadingsToTOC(headings, tree = [], currentLevel = 2) { /** * @type {import('../types').TOCTreeItem} */ + // @ts-expect-error - headings[0] exists const heading = headings[0] if (heading.level < currentLevel) { return tree diff --git a/apps/blog/tsconfig.json b/apps/blog/tsconfig.json index 2e1157d1..278b1e17 100644 --- a/apps/blog/tsconfig.json +++ b/apps/blog/tsconfig.json @@ -10,6 +10,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, + "noUncheckedIndexedAccess": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", diff --git a/apps/gamehub-client/tsconfig.json b/apps/gamehub-client/tsconfig.json index 3f4abda7..3a2210bc 100644 --- a/apps/gamehub-client/tsconfig.json +++ b/apps/gamehub-client/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", diff --git a/apps/ligretto-core-backend/tsconfig.json b/apps/ligretto-core-backend/tsconfig.json index 71978c18..ef44f17a 100644 --- a/apps/ligretto-core-backend/tsconfig.json +++ b/apps/ligretto-core-backend/tsconfig.json @@ -18,6 +18,7 @@ "declaration": false, "moduleResolution": "node", "strictNullChecks": true, + "noUncheckedIndexedAccess": true, "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, diff --git a/apps/ligretto-frontend/src/features/leader-board/ui/LeaderList/LeaderListTable.tsx b/apps/ligretto-frontend/src/features/leader-board/ui/LeaderList/LeaderListTable.tsx index 1b9c48f3..76b8316b 100644 --- a/apps/ligretto-frontend/src/features/leader-board/ui/LeaderList/LeaderListTable.tsx +++ b/apps/ligretto-frontend/src/features/leader-board/ui/LeaderList/LeaderListTable.tsx @@ -15,7 +15,7 @@ const leaderListTableSpecs = [ { title: '#', size: { xs: 2, md: 1.5 } }, { title: 'Player', size: { xs: 7.7, md: 8.7 } }, { title: 'Points', size: { xs: 1.8, md: 1.8 } }, -] +] as const const placeImages = [firstLevelImg, secondLevelImg, thirdLevelImg] diff --git a/apps/ligretto-frontend/src/features/playground/ui/Playground.tsx b/apps/ligretto-frontend/src/features/playground/ui/Playground.tsx index df4d5adc..c6fe97d3 100644 --- a/apps/ligretto-frontend/src/features/playground/ui/Playground.tsx +++ b/apps/ligretto-frontend/src/features/playground/ui/Playground.tsx @@ -9,7 +9,7 @@ export interface PlaygroundProps { onDeckClick: (playgroundDeckIndex: number) => void } -const getLastCard = (deck: CardsDeck | null): Card | undefined => last(deck?.cards) +const getLastCard = (deck: CardsDeck | null | undefined): Card | undefined => last(deck?.cards) export const Playground: React.FC = ({ cardsDecks, onDeckClick }) => { const cards: (Card | undefined)[] = useMemo(() => { diff --git a/apps/ligretto-frontend/tsconfig.json b/apps/ligretto-frontend/tsconfig.json index 40ee0264..4f60c779 100644 --- a/apps/ligretto-frontend/tsconfig.json +++ b/apps/ligretto-frontend/tsconfig.json @@ -10,6 +10,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, diff --git a/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts b/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts index 54b7b3af..4c8c9810 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/__tests__/games-contoller.spec.ts @@ -60,9 +60,9 @@ describe('Games Controller', () => { expect(state).toMatchSnapshot() expect(socketMockImpl.emit).toBeCalledTimes(2) - expect(socketMockImpl.emit).toBeCalledWith('event', createRoomSuccessAction({ game: state.games[gameId] })) + expect(socketMockImpl.emit).toBeCalledWith('event', createRoomSuccessAction({ game: state.games[gameId]! })) expect(socketMockImpl.to).toBeCalledWith(SOCKET_ROOM_LOBBY) - expect(socketMockImpl.emit).toBeCalledWith('event', updateRoomsAction({ rooms: [gameToRoom(state.games[gameId])] })) + expect(socketMockImpl.emit).toBeCalledWith('event', updateRoomsAction({ rooms: [gameToRoom(state.games[gameId]!)] })) }) it('Should emit createRoomErrorAction if room already exists', async () => { @@ -216,8 +216,8 @@ describe('Games Controller', () => { const state = await database.get(storage => storage) expect(state).toMatchSnapshot() expect(socketOne.to).toBeCalledTimes(4) - expect(socketOne.emit).toBeCalledWith('event', updateRoomsAction({ rooms: [gameToRoom(state.games[roomUuid])] })) - expect(socketOne.emit).toBeCalledWith('event', updateGameAction(state.games[roomUuid])) + expect(socketOne.emit).toBeCalledWith('event', updateRoomsAction({ rooms: [gameToRoom(state.games[roomUuid]!)] })) + expect(socketOne.emit).toBeCalledWith('event', updateGameAction(state.games[roomUuid]!)) }) it('Should create a relevant game state if one of two players leaved', async () => { diff --git a/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts b/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts index d0de0072..c314af9b 100644 --- a/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts +++ b/apps/ligretto-gameplay-backend/src/controllers/gameplay-controller.ts @@ -42,6 +42,10 @@ export class GameplayController extends Controller { private async updateGame(socket: Socket, gameId: string, gameState?: Game) { const game = gameState || (await this.gameService.getGame(gameId)) + if (!game) { + return + } + const action = updateGameAction(game) socket.to(gameId).emit('event', action) diff --git a/apps/ligretto-gameplay-backend/src/database/database.ts b/apps/ligretto-gameplay-backend/src/database/database.ts index ef3e2043..9eedd590 100644 --- a/apps/ligretto-gameplay-backend/src/database/database.ts +++ b/apps/ligretto-gameplay-backend/src/database/database.ts @@ -43,11 +43,8 @@ export class Database implements Database { private listeners: Record void> = {} private notifyListeners() { - for (const listenerKey in this.listeners) { - if (this.listeners.hasOwnProperty(listenerKey)) { - const listener = this.listeners[listenerKey] - listener(this.storage) - } + for (const listener of Object.values(this.listeners)) { + listener(this.storage) } } diff --git a/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts b/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts index 6a3326ef..f0e947a6 100644 --- a/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/game/game.repo.ts @@ -19,6 +19,10 @@ export class GameRepository { async updateGame(gameId: UUID, updater: (game: Game) => Game): Promise { const game = await this.getGame(gameId) + if (!game) { + throw new Error(`Game with id ${gameId} not found`) + } + return this.database.set(storage => (storage.games[gameId] = updater(game))) } diff --git a/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts b/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts index 444f4496..4d67b316 100644 --- a/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts +++ b/apps/ligretto-gameplay-backend/src/entities/game/game.service.ts @@ -147,6 +147,10 @@ export class GameService { async getRoundResult(gameId: UUID) { const game = await this.getGame(gameId) + if (!game) { + throw new Error(`Game with id ${gameId} not found`) + } + const initialScoresByPlayer = Object.keys(game.players).reduce>((scores, playerId) => ({ ...scores, [playerId]: 0 }), {}) const clearPlaygroundDecks = game.playground.decks.filter(nonNullable) diff --git a/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts b/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts index d1171034..55d0eda4 100644 --- a/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/player/player.repo.ts @@ -8,45 +8,63 @@ export class PlayerRepository { @inject(IOC_TYPES.Database) private database: Database async getPlayer(gameId: UUID, playerId: UUID) { - return this.database.get(storage => storage.games[gameId].players[playerId]) + return this.database.get(storage => storage.games[gameId]?.players?.[playerId]) } async getCards(gameId: UUID, playerId: UUID) { - return this.database.get(storage => storage.games[gameId].players[playerId]?.cards) + return this.database.get(storage => storage.games[gameId]?.players?.[playerId]?.cards) } async getCard(gameId: UUID, playerId: UUID, position: number) { - return this.database.get(storage => storage.games[gameId].players[playerId]?.cards[position]) + return this.database.get(storage => storage.games[gameId]?.players?.[playerId]?.cards?.[position]) } async addCard(gameId: UUID, playerId: UUID, card: Card, position: number) { - return this.database.set(storage => storage.games[gameId].players[playerId]?.cards.splice(position, 1, card)) + return this.database.set(storage => { + const player = storage.games[gameId]?.players?.[playerId] + const cards = player?.cards + + if (!cards) { + return + } + + return cards.splice(position, 1, card) + }) } async removeCard(gameId: UUID, playerId: UUID, position: number) { - return this.database.set(storage => storage.games[gameId].players[playerId]?.cards.splice(position, 1, null)) + return this.database.set(storage => { + const player = storage.games[gameId]?.players?.[playerId] + const cards = player?.cards + + if (!cards) { + return + } + + return cards.splice(position, 1, null) + }) } async getLigrettoDeck(gameId: UUID, playerId: UUID) { - return this.database.get(storage => storage.games[gameId].players[playerId]?.ligrettoDeck) + return this.database.get(storage => storage.games[gameId]?.players?.[playerId]?.ligrettoDeck) } async removeCardFromLigrettoDeck(gameId: UUID, playerId: UUID) { - await this.database.set(storage => storage.games[gameId].players[playerId]?.ligrettoDeck.cards.pop()) + await this.database.set(storage => storage.games[gameId]?.players?.[playerId]?.ligrettoDeck?.cards.pop()) return (await this.getLigrettoDeck(gameId, playerId))?.cards.length } async getStackDeck(gameId: UUID, playerId: UUID) { - return this.database.get(storage => storage.games[gameId].players[playerId]?.stackDeck) + return this.database.get(storage => storage.games[gameId]?.players?.[playerId]?.stackDeck) } async getStackOpenDeck(gameId: UUID, playerId: UUID) { - return this.database.get(storage => storage.games[gameId].players[playerId]?.stackOpenDeck) + return this.database.get(storage => storage.games[gameId]?.players?.[playerId]?.stackOpenDeck) } async removeCardFromStackOpenDeck(gameId: UUID, playerId: UUID) { - return this.database.set(storage => storage.games[gameId].players[playerId]?.stackOpenDeck.cards.pop()) + return this.database.set(storage => storage.games[gameId]?.players?.[playerId]?.stackOpenDeck?.cards.pop()) } async updateStackDeck(gameId: UUID, playerId: UUID, updater: (cardsDeck: CardsDeck) => CardsDeck) { @@ -55,7 +73,7 @@ export class PlayerRepository { return } return this.database.set(storage => { - const player = storage.games[gameId].players[playerId] + const player = storage.games[gameId]?.players?.[playerId] if (!player) { return } @@ -69,7 +87,7 @@ export class PlayerRepository { return } return this.database.set(storage => { - const player = storage.games[gameId].players[playerId] + const player = storage.games[gameId]?.players?.[playerId] if (!player) { return } diff --git a/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts b/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts index 425c6197..a383d67d 100644 --- a/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts +++ b/apps/ligretto-gameplay-backend/src/entities/playground/playground.repo.ts @@ -8,24 +8,35 @@ export class PlaygroundRepository { @inject(IOC_TYPES.Database) private database: Database getDecks(gameId: UUID) { - return this.database.get(storage => storage.games[gameId].playground.decks) + return this.database.get(storage => storage.games[gameId]?.playground.decks) } getDeck(gameId: UUID, position: number) { - return this.database.get(storage => storage.games[gameId].playground.decks[position]) + return this.database.get(storage => storage.games[gameId]?.playground.decks?.[position]) } addDroppedDeck(gameId: UUID, cardsDeck: CardsDeck) { return this.database.set(storage => { - const decks = storage.games[gameId].playground.droppedDecks - decks.push(cardsDeck) - return decks + const game = storage.games[gameId] + const droppedDecks = game?.playground.droppedDecks + + if (!droppedDecks) { + return + } + + droppedDecks.push(cardsDeck) + return droppedDecks }) } removeDeck(gameId: UUID, position: number) { return this.database.set(storage => { - storage.games[gameId].playground.decks[position] = null + const game = storage.games[gameId] + if (!game) { + return + } + + game.playground.decks[position] = null }) } @@ -33,9 +44,14 @@ export class PlaygroundRepository { const deck = await this.getDeck(gameId, position) return this.database.set(storage => { - const updated = updater(deck) + const game = storage.games[gameId] + if (!game) { + return + } + + const updated = updater(deck ?? null) console.log('Updated deck', position, updated) - storage.games[gameId].playground.decks[position] = updated + game.playground.decks[position] = updated }) } } diff --git a/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts b/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts index 8bd1fe1b..36859a24 100644 --- a/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts +++ b/apps/ligretto-gameplay-backend/src/entities/playground/playground.service.ts @@ -22,13 +22,18 @@ export class PlaygroundService { async findAvailableDeckIndex(gameId: UUID, card: Card) { const decks = await this.getDecks(gameId) - return decks.findIndex(deck => isDeckAvailable(deck, card)) + + if (!decks) { + return -1 + } + + return decks.findIndex(deck => isDeckAvailable(deck ?? null, card)) } async putCard(gameId: UUID, card: Card, deckIndex: number) { const deck = await this.playgroundRepository.getDeck(gameId, deckIndex) - if (!isDeckAvailable(deck, card)) { + if (!isDeckAvailable(deck ?? null, card)) { return } diff --git a/apps/ligretto-gameplay-backend/tsconfig.json b/apps/ligretto-gameplay-backend/tsconfig.json index 07b1c6c4..e9c90acc 100644 --- a/apps/ligretto-gameplay-backend/tsconfig.json +++ b/apps/ligretto-gameplay-backend/tsconfig.json @@ -14,6 +14,7 @@ "strict": true, // Not compatible with inversify "strictPropertyInitialization": false, + "noUncheckedIndexedAccess": true, "skipLibCheck": true }, "include": ["src/**/*"] diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json index 27afef23..69aa217f 100644 --- a/packages/analytics/tsconfig.json +++ b/packages/analytics/tsconfig.json @@ -8,6 +8,7 @@ "outDir": "./build", "sourceMap": true, "strict": true, + "noUncheckedIndexedAccess": true, "allowSyntheticDefaultImports": true }, "exclude": ["node_modules", "build"], diff --git a/packages/ligretto-shared/tsconfig.json b/packages/ligretto-shared/tsconfig.json index ff650e17..6642b773 100644 --- a/packages/ligretto-shared/tsconfig.json +++ b/packages/ligretto-shared/tsconfig.json @@ -6,7 +6,8 @@ "module": "commonjs", "moduleResolution": "node", "outDir": "./build", - "sourceMap": true + "sourceMap": true, + "noUncheckedIndexedAccess": true }, "exclude": ["node_modules", "build"], "include": ["src/**/*.ts"] diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 759e18e5..fbb08f88 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -14,6 +14,7 @@ "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "baseUrl": "./src", "noImplicitAny": true diff --git a/tsconfig.json b/tsconfig.json index 43be261b..6e6333f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "files": ["typings/font.d.ts", "typings/images.d.ts"], "compilerOptions": { - "jsx": "react-jsx" + "jsx": "react-jsx", + "noUncheckedIndexedAccess": true } }