diff --git a/README.md b/README.md index 5bade59..f1de52d 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ Fields: - [x] [Server-side Player model implementation](https://github.com/alan-hadyk/side-stacker-game/pull/6) - [x] [Server-side Game model implementation](https://github.com/alan-hadyk/side-stacker-game/pull/7) - [x] [Server-side Move model implementation](https://github.com/alan-hadyk/side-stacker-game/pull/8) -- [ ] Server-side Player controllers and services implementation +- [x] [Server-side Player controllers and services implementation](https://github.com/alan-hadyk/side-stacker-game/pull/9) - [ ] Server-side Game controllers and services implementation - [ ] Server-side Move controllers and services implementation - [ ] Server routes implementation diff --git a/server/package.json b/server/package.json index 1dcfd57..0eeac2a 100644 --- a/server/package.json +++ b/server/package.json @@ -6,12 +6,17 @@ "description": "Server for Side-Stacker game", "dependencies": { "express": "^4.18.2", + "lodash": "^4.17.21", "slonik": "^34.0.1", + "socket.io": "^4.7.1", + "uuid": "^9.0.0", "zod": "^3.21.4" }, "devDependencies": { "@types/express": "^4.17.17", + "@types/lodash": "^4.14.195", "@types/node": "^20.4.2", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "eslint": "^8.45.0", diff --git a/server/src/clients/@types/websocketsServer.ts b/server/src/clients/@types/websocketsServer.ts new file mode 100644 index 0000000..ac1780c --- /dev/null +++ b/server/src/clients/@types/websocketsServer.ts @@ -0,0 +1,12 @@ +export interface ServerToClientEvents { + invalidateQuery: (queryKeys: { + entity: string[] + id?: string | number + }) => void +} + +export interface ClientToServerEvents {} + +export interface InterServerEvents {} + +export interface SocketData {} diff --git a/server/src/clients/websocketsServer.ts b/server/src/clients/websocketsServer.ts new file mode 100644 index 0000000..d5ec74a --- /dev/null +++ b/server/src/clients/websocketsServer.ts @@ -0,0 +1,35 @@ +import { + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData, +} from "@app/clients/@types/websocketsServer" +import { IncomingMessage, Server, ServerResponse } from "http" +import { Server as SocketIOServer } from "socket.io" + +let websocketsServer: SocketIOServer< + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData +> + +export const createWebsocketsServer = ( + httpServer: Server, +) => { + websocketsServer = new SocketIOServer< + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData + >(httpServer, { + connectionStateRecovery: { + // the backup duration of the sessions and the packets + maxDisconnectionDuration: 10 * 60 * 1000, + // whether to skip middlewares upon successful recovery + skipMiddlewares: true, + }, + }) +} + +export { websocketsServer } diff --git a/server/src/config/app.ts b/server/src/config/app.ts index c683db5..d5aeb09 100644 --- a/server/src/config/app.ts +++ b/server/src/config/app.ts @@ -1,4 +1,6 @@ export const appConfig = { - host: "127.0.0.1", - port: 3000, + httpServer: { + host: "127.0.0.1", + port: 3000, + }, } diff --git a/server/src/features/games/@types/gameModel.ts b/server/src/features/games/@types/gameModel.ts index 6d2b634..b2ef4d1 100644 --- a/server/src/features/games/@types/gameModel.ts +++ b/server/src/features/games/@types/gameModel.ts @@ -2,10 +2,20 @@ import { OrderDirection } from "@app/features/@types/models" import { Game, GameStateEnum } from "@app/features/games/@types/gameObject" export interface GameModelGetAll { - filters?: Record< - keyof Pick, - string | GameStateEnum + filters?: Partial< + Record< + keyof Pick< + Game, + | "player1_id" + | "player2_id" + | "current_player_id" + | "current_game_state" + | "winner_id" + >, + string | GameStateEnum + > > + filterType?: "AND" | "OR" limit?: number offset?: number orderBy?: keyof Game diff --git a/server/src/features/games/gameModel.ts b/server/src/features/games/gameModel.ts index 24f1979..23307cd 100644 --- a/server/src/features/games/gameModel.ts +++ b/server/src/features/games/gameModel.ts @@ -60,6 +60,7 @@ export class GameModel { static getAll = ({ filters, + filterType = "AND", limit = 40, offset = 0, orderBy = "created_at", @@ -80,7 +81,10 @@ export class GameModel { const query = sql.typeAlias("game")` SELECT * FROM games - WHERE ${sql.join(filtersFragments, sql.unsafe`, `)} + WHERE ${sql.join( + filtersFragments, + filterType === "AND" ? sql.unsafe`, ` : sql.unsafe` OR `, + )} ORDER BY ${sql.identifier([orderBy])} ${sql.unsafe([direction])} LIMIT ${limit} OFFSET ${offset} diff --git a/server/src/features/players/playerController.ts b/server/src/features/players/playerController.ts new file mode 100644 index 0000000..baf82a8 --- /dev/null +++ b/server/src/features/players/playerController.ts @@ -0,0 +1,228 @@ +import { websocketsServer } from "@app/clients/websocketsServer" +import { PlayerModelGetAll } from "@app/features/players/@types/playerModel" +import { PlayerModel } from "@app/features/players/playerModel" +import { PlayerObject } from "@app/features/players/playerObject" +import { convertObjectToObjectWithIsoDates } from "@app/helpers/objects/convertObjectToObjectWithIsoDates" +import { GameService } from "@app/services/gameService" +import { RequestValidationService } from "@app/services/requestValidationService" +import { Request, Response, NextFunction } from "express" +import { v4 as uuidv4 } from "uuid" + +export class PlayerController { + static create = async (req: Request, res: Response, next: NextFunction) => { + try { + RequestValidationService.validateQuery(req.query, [], [], []) + const { username } = RequestValidationService.validateBody( + req.body, + PlayerObject.pick({ username: true }), + ) + + const session_id = uuidv4() + + const newPlayer = await PlayerModel.create({ + session_id, + username, + }) + + const newPlayerWithIsoDates = { + ...newPlayer, + ...convertObjectToObjectWithIsoDates(newPlayer, [ + "created_at", + "deleted_at", + "last_active_at", + ]), + } + + // Emit an event to all connected clients to invalidate the players query + websocketsServer.emit("invalidateQuery", { + entity: ["players", "list"], + }) + + res.json(newPlayerWithIsoDates) + } catch (error) { + next(error) + } + } + + static delete = async (req: Request, res: Response, next: NextFunction) => { + try { + RequestValidationService.validateQuery(req.query, [], [], []) + const { session_id } = RequestValidationService.validateBody( + req.body, + PlayerObject.omit({ + created_at: true, + deleted_at: true, + last_active_at: true, + player_id: true, + username: true, + }), + ) + const { player_id } = RequestValidationService.validateParams( + req.params, + PlayerObject.pick({ player_id: true }), + ) + + const deletedPlayer = await PlayerModel.delete(player_id, session_id) + + const deletedPlayerWithIsoDates = { + ...deletedPlayer, + ...convertObjectToObjectWithIsoDates(deletedPlayer, [ + "created_at", + "deleted_at", + "last_active_at", + ]), + } + + await GameService.removeDeletedPlayerFromGames(deletedPlayer) + + // Emit an event to all connected clients to invalidate the players query + websocketsServer.emit("invalidateQuery", { + entity: ["players", "list"], + }) + websocketsServer.emit("invalidateQuery", { + entity: ["players", "detail"], + id: deletedPlayer.player_id, + }) + + res.json(deletedPlayerWithIsoDates) + } catch (error) { + next(error) + } + } + + static getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const { limit, offset, orderBy, orderDirection } = + RequestValidationService.validateQuery( + req.query, + ["limit", "offset", "orderDirection", "orderBy"], + [ + "username", + "created_at", + "deleted_at", + "last_active_at", + "player_id", + ], + [], + ) + RequestValidationService.validateBody( + req.body, + PlayerObject.omit({ + created_at: true, + deleted_at: true, + last_active_at: true, + player_id: true, + session_id: true, + username: true, + }), + ) + + const players = await PlayerModel.getAll({ + limit, + offset, + orderBy: orderBy as PlayerModelGetAll["orderBy"], + orderDirection, + }) + + const playersWithIsoDates = players.map((player) => { + const { created_at, deleted_at, last_active_at, player_id, username } = + player + + return { + player_id, + username, + ...convertObjectToObjectWithIsoDates( + { created_at, deleted_at, last_active_at }, + ["created_at", "deleted_at", "last_active_at"], + ), + } + }) + + res.json(playersWithIsoDates) + } catch (error) { + next(error) + } + } + + static getById = async (req: Request, res: Response, next: NextFunction) => { + try { + RequestValidationService.validateQuery(req.query, [], [], []) + RequestValidationService.validateBody( + req.body, + PlayerObject.omit({ + created_at: true, + deleted_at: true, + last_active_at: true, + player_id: true, + session_id: true, + username: true, + }), + ) + const params = RequestValidationService.validateParams( + req.params, + PlayerObject.pick({ player_id: true }), + ) + + const player = await PlayerModel.getById(params.player_id) + const { created_at, deleted_at, last_active_at, player_id, username } = + player + + const playerWithIsoDates = { + player_id, + username, + ...convertObjectToObjectWithIsoDates( + { created_at, deleted_at, last_active_at }, + ["created_at", "deleted_at", "last_active_at"], + ), + } + + res.json(playerWithIsoDates) + } catch (error) { + next(error) + } + } + + static update = async (req: Request, res: Response, next: NextFunction) => { + try { + RequestValidationService.validateQuery(req.query, [], [], []) + const body = RequestValidationService.validateBody( + req.body, + PlayerObject.pick({ + username: true, + }), + ) + const params = RequestValidationService.validateParams( + req.params, + PlayerObject.pick({ player_id: true }), + ) + + const player = await PlayerModel.update(params.player_id, { + username: body.username, + }) + const { created_at, deleted_at, last_active_at, player_id, username } = + player + + const playerWithIsoDates = { + player_id, + username, + ...convertObjectToObjectWithIsoDates( + { created_at, deleted_at, last_active_at }, + ["created_at", "deleted_at", "last_active_at"], + ), + } + + // Emit an event to all connected clients to invalidate the players query + websocketsServer.emit("invalidateQuery", { + entity: ["players", "list"], + }) + websocketsServer.emit("invalidateQuery", { + entity: ["players", "detail"], + id: player_id, + }) + + res.json(playerWithIsoDates) + } catch (error) { + next(error) + } + } +} diff --git a/server/src/features/players/playerModel.ts b/server/src/features/players/playerModel.ts index 3f83e9e..4f4cc1a 100644 --- a/server/src/features/players/playerModel.ts +++ b/server/src/features/players/playerModel.ts @@ -31,14 +31,20 @@ export class PlayerModel { return rows[0] }) - static delete = (player_id: Player["player_id"]): Promise => + static delete = ( + player_id: Player["player_id"], + session_id: Player["session_id"], + ): Promise => pool.connect(async (connection) => { - const fragments = [sql.fragment`deleted_at = NOW()`] + const fragments = [ + sql.fragment`session_id = NULL`, + sql.fragment`deleted_at = NOW()`, + ] const query = sql.typeAlias("player")` UPDATE players SET ${sql.join(fragments, sql.unsafe`, `)} - WHERE player_id = ${player_id} + WHERE player_id = ${player_id}, session_id = ${session_id} RETURNING * ` @@ -54,7 +60,7 @@ export class PlayerModel { static getAll = ({ limit = 20, offset = 0, - orderBy = "created_at", + orderBy = "last_active_at", orderDirection = OrderDirection.DESC, }: PlayerModelGetAll): Promise => pool.connect(async (connection) => { @@ -84,24 +90,10 @@ export class PlayerModel { static update = ( player_id: Player["player_id"], - { - last_active_at, - session_id, - username, - }: Partial>, + { username }: Partial>, ): Promise => pool.connect(async (connection) => { - const fragments = [] - - fragments.push( - last_active_at !== undefined - ? sql.fragment`last_active_at = ${last_active_at}` - : sql.fragment`last_active_at = NOW()`, - ) - - if (session_id !== undefined) { - fragments.push(sql.fragment`session_id = ${session_id}`) - } + const fragments = [sql.fragment`last_active_at = NOW()`] if (username !== undefined) { fragments.push(sql.fragment`username = ${username}`) diff --git a/server/src/helpers/dates/convertUnixTimestampToDate.ts b/server/src/helpers/dates/convertUnixTimestampToDate.ts new file mode 100644 index 0000000..dea716f --- /dev/null +++ b/server/src/helpers/dates/convertUnixTimestampToDate.ts @@ -0,0 +1,2 @@ +export const convertUnixTimestampToDate = (unixTimestamp: number) => + new Date(unixTimestamp).toISOString() diff --git a/server/src/helpers/numbers/isConvertableToNumber.ts b/server/src/helpers/numbers/isConvertableToNumber.ts new file mode 100644 index 0000000..037b5fd --- /dev/null +++ b/server/src/helpers/numbers/isConvertableToNumber.ts @@ -0,0 +1 @@ +export const isConvertableToNumber = (str: T) => !Number.isNaN(Number(str)) diff --git a/server/src/helpers/objects/convertObjectToObjectWithIsoDates.ts b/server/src/helpers/objects/convertObjectToObjectWithIsoDates.ts new file mode 100644 index 0000000..3ed339b --- /dev/null +++ b/server/src/helpers/objects/convertObjectToObjectWithIsoDates.ts @@ -0,0 +1,32 @@ +import { convertUnixTimestampToDate } from "@app/helpers/dates/convertUnixTimestampToDate" +import { isConvertableToNumber } from "@app/helpers/numbers/isConvertableToNumber" + +export const convertObjectToObjectWithIsoDates = < + K extends PropertyKey, + V extends string | number, +>( + obj: Record, + dateFields: Array, +) => { + const transformedObject = { ...obj } + + dateFields.forEach((dateField) => { + if (dateField in obj) { + try { + const value = obj[dateField] + + if (isConvertableToNumber(value)) { + const valueAsNumber = Number(value) + + transformedObject[dateField] = convertUnixTimestampToDate( + valueAsNumber, + ) as V + } + } catch (error) { + console.error(error) + } + } + }) + + return transformedObject as Record +} diff --git a/server/src/index.ts b/server/src/index.ts index 6fd6ed0..e28f2b0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,25 +1,38 @@ +import { + createWebsocketsServer, + websocketsServer, +} from "@app/clients/websocketsServer" import { config } from "@app/config" import { initializers } from "@app/initializers" import { useMiddlewares } from "@app/middlewares" -import { handleErrorsMiddleware } from "@app/middlewares/handleErrors" +import { handleHttpErrorsMiddleware } from "@app/middlewares/handleHttpErrors" import express from "express" +import { createServer } from "http" const app = express() -const { host, port } = config.appConfig + +const httpServer = createServer(app) +createWebsocketsServer(httpServer) const startServer = async () => { // Initializers await Promise.all(initializers.map((initializer) => initializer())) // Middlewares - await useMiddlewares(app) + await useMiddlewares(app, websocketsServer) + + // HTTP Errors middleware + app.use(handleHttpErrorsMiddleware) + + const { appConfig } = config + const { host, port } = appConfig.httpServer - // Errors middleware - app.use(handleErrorsMiddleware) + websocketsServer.listen(port) // Run the server at given host and port - app.listen(port, host, () => { - console.log(`Server running at http://${host}:${port}/`) + httpServer.listen(port, host, () => { + console.log(`HTTP server running at http://${host}:${port}/`) + console.log(`WS server running at ws://${host}:${port}/`) }) } diff --git a/server/src/middlewares/@types/handleErrors.ts b/server/src/middlewares/@types/handleHttpErrors.ts similarity index 100% rename from server/src/middlewares/@types/handleErrors.ts rename to server/src/middlewares/@types/handleHttpErrors.ts diff --git a/server/src/middlewares/handleErrors.ts b/server/src/middlewares/handleHttpErrors.ts similarity index 93% rename from server/src/middlewares/handleErrors.ts rename to server/src/middlewares/handleHttpErrors.ts index abf1ce5..fcda19d 100644 --- a/server/src/middlewares/handleErrors.ts +++ b/server/src/middlewares/handleHttpErrors.ts @@ -1,4 +1,4 @@ -import { Err } from "@app/middlewares/@types/handleErrors" +import { Err } from "@app/middlewares/@types/handleHttpErrors" import { NextFunction, Request, Response } from "express" import { BackendTerminatedError, @@ -13,7 +13,7 @@ import { UniqueIntegrityConstraintViolationError, } from "slonik" -export const handleErrorsMiddleware = ( +export const handleHttpErrorsMiddleware = ( error: Err, _req: Request, res: Response, diff --git a/server/src/middlewares/handleWsErrors.ts b/server/src/middlewares/handleWsErrors.ts new file mode 100644 index 0000000..d848f04 --- /dev/null +++ b/server/src/middlewares/handleWsErrors.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const handleWsErrors = (err: any) => { + console.error("Websockets error:") + console.log(err?.req) // the request object + console.log(err?.code) // the error code, for example 1 + console.log(err?.message) // the error message, for example "Session ID unknown" + console.log(err?.context) // some additional error context +} diff --git a/server/src/middlewares/index.ts b/server/src/middlewares/index.ts index 9465788..492ffea 100644 --- a/server/src/middlewares/index.ts +++ b/server/src/middlewares/index.ts @@ -1,6 +1,35 @@ +import { + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData, +} from "@app/clients/@types/websocketsServer" +import { handleWsErrors } from "@app/middlewares/handleWsErrors" import express, { Express } from "express" +import { Server as SocketIOServer } from "socket.io" -export const useMiddlewares = async (app: Express) => { +export const useMiddlewares = async ( + app: Express, + websocketsServer: SocketIOServer< + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData + >, +) => { // Parse application/json app.use(express.json()) + + // Websockets logging + websocketsServer.engine.on("connection_error", handleWsErrors) + websocketsServer.on("connection", (socket) => { + if (socket.recovered) { + // recovery was successful: socket.id, socket.rooms and socket.data were restored + console.log("websocketsServer recovery was successful") + } else { + console.log("websocketsServer connection") + } + + console.log({ socket }) + }) } diff --git a/server/src/services/gameService.ts b/server/src/services/gameService.ts new file mode 100644 index 0000000..a0c1723 --- /dev/null +++ b/server/src/services/gameService.ts @@ -0,0 +1,41 @@ +import { websocketsServer } from "@app/clients/websocketsServer" +import { GameModel } from "@app/features/games/gameModel" +import { GameStateEnum } from "@app/features/games/gameObject" +import { Player } from "@app/features/players/@types/playerObject" + +export class GameService { + static removeDeletedPlayerFromGames = async (deletedPlayer: Player) => { + const gamesWithDeletedPlayer = await GameModel.getAll({ + filterType: "OR", + filters: { + current_game_state: GameStateEnum.enum.in_progress, + current_player_id: deletedPlayer.player_id, + player1_id: deletedPlayer.player_id, + player2_id: deletedPlayer.player_id, + }, + }) + + if (gamesWithDeletedPlayer.length > 0) { + await Promise.all( + gamesWithDeletedPlayer.map((game) => { + const field = Object.entries(game).find( + ([, value]) => value === deletedPlayer.player_id, + )?.[0] + + if (field) { + return GameModel.update(game.game_id, { + [field]: "", + }) + } + + return game + }), + ) + + // Emit an event to all connected clients to invalidate the games query + websocketsServer.emit("invalidateQuery", { + entity: ["games", "list"], + }) + } + } +} diff --git a/server/src/services/requestValidationService.ts b/server/src/services/requestValidationService.ts new file mode 100644 index 0000000..03f8454 --- /dev/null +++ b/server/src/services/requestValidationService.ts @@ -0,0 +1,147 @@ +import { ValidationError } from "@app/errors/definitions/validationError" +import { OrderDirection } from "@app/features/@types/models" +import { isConvertableToNumber } from "@app/helpers/numbers/isConvertableToNumber" +import { Request } from "express" +import isEmpty from "lodash/isEmpty" +import omitBy from "lodash/omitBy" +import isNil from "lodash/isNil" +import { ZodRawShape, z } from "zod" + +export class RequestValidationService { + static validateBody = ( + body: Request["body"], + entityObject: z.ZodObject, + ) => { + const incorrectFields: string[] = [] + const entityObjectKeys: string[] = entityObject.keyof()._def.values + + Object.keys(body).forEach((bodyKey) => { + if (!entityObjectKeys.includes(bodyKey as never)) { + incorrectFields.push(bodyKey) + } + }) + + if (!isEmpty(incorrectFields)) { + throw new ValidationError(incorrectFields) + } + + const payloadWithoutEmptyFields = omitBy(body, isNil) + + entityObject.parse(payloadWithoutEmptyFields) + + return payloadWithoutEmptyFields as z.infer + } + + static validateQuery = ( + query: Request["query"], + allowedQueryParams: string[], + objectKeys: T[], + allowedFilters: string[], + ) => { + const disallowedQueryParams = Object.keys(query).filter( + (queryParam) => !allowedQueryParams.includes(queryParam), + ) + + const errors: string[] = [] + + if (!isEmpty(disallowedQueryParams)) { + errors.push( + `Incorrect query params: ${JSON.stringify(disallowedQueryParams)}`, + ) + } + + if (query.filters && allowedQueryParams.includes("filters")) { + for (const [key, value] of Object.entries(query.filters)) { + if (!allowedFilters.includes(key)) { + errors.push(`incorrect filter query param: ${key} - ${value}`) + } + } + } + + if ( + query.limit && + allowedQueryParams.includes("limit") && + !isConvertableToNumber(query.limit) + ) { + errors.push("limit query param should be a number") + } + + if ( + query.offset && + allowedQueryParams.includes("offset") && + !isConvertableToNumber(query.offset) + ) { + errors.push("offset query param should be a number") + } + + const orderDirectionObjectValues = Object.values(OrderDirection) + + if ( + query.orderDirection && + allowedQueryParams.includes("orderDirection") && + !orderDirectionObjectValues.includes( + query.orderDirection as OrderDirection, + ) + ) { + errors.push( + `order query param should be one of the following values: ${JSON.stringify( + orderDirectionObjectValues, + )}`, + ) + } + + if ( + query.orderBy && + allowedQueryParams.includes("orderBy") && + !objectKeys.includes(query.orderBy as T) + ) { + errors.push( + `orderBy query param should be one of the following values: ${JSON.stringify( + objectKeys, + )}`, + ) + } + + if (!isEmpty(errors)) { + throw new ValidationError(errors) + } + + const { filters, limit, offset, orderBy, orderDirection } = query + + return { + ...(filters && { + filters: filters as Record, + }), + ...(limit && { limit: Number(limit) }), + ...(offset && { offset: Number(offset) }), + ...(orderBy && { orderBy: orderBy as T }), + ...(orderDirection && { + orderDirection: orderDirection as OrderDirection, + }), + } + } + + static validateParams = ( + params: Request["params"], + entityObject: z.ZodObject, + ) => { + const incorrectParams: string[] = [] + const entityObjectKeys: string[] = entityObject.keyof()._def.values + + Object.keys(params).forEach((bodyKey) => { + if (!entityObjectKeys.includes(bodyKey as never)) { + incorrectParams.push(bodyKey) + } + }) + + if (!isEmpty(incorrectParams)) { + throw new ValidationError(incorrectParams) + } + + const payloadWithoutEmptyFields = omitBy(params, isNil) + + entityObject.parse(payloadWithoutEmptyFields) + + return payloadWithoutEmptyFields as z.infer + } +} diff --git a/server/yarn.lock b/server/yarn.lock index cc4bafc..57d5e8d 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -116,6 +116,11 @@ picocolors "^1.0.0" tslib "^2.6.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -151,6 +156,18 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + "@types/express-serve-static-core@^4.17.33": version "4.17.35" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" @@ -186,6 +203,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.14.195": + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -196,7 +218,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/node@*", "@types/node@^20.4.2": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@^20.4.2": version "20.4.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw== @@ -252,6 +274,11 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/uuid@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" + integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== + "@typescript-eslint/eslint-plugin@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz#96f3ca6615717659d06c9f7161a1d14ab0c49c66" @@ -393,7 +420,7 @@ "@typescript-eslint/types" "6.1.0" eslint-visitor-keys "^3.4.1" -accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -527,6 +554,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + big-integer@^1.6.44: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -689,6 +721,19 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -717,7 +762,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -818,6 +863,27 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +engine.io-parser@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.1.0.tgz#d593d6372d7f79212df48f807b8cace1ea1cb1b8" + integrity sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w== + +engine.io@~6.5.0: + version "6.5.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.1.tgz#59725f8593ccc891abb47f1efcdc52a089525a56" + integrity sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.1.0" + ws "~8.11.0" + es-abstract@^1.19.0, es-abstract@^1.20.4: version "1.22.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" @@ -1717,6 +1783,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1860,6 +1931,11 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" @@ -2444,6 +2520,34 @@ slonik@^34.0.1: serialize-error "^8.0.0" through2 "^4.0.2" +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.1.tgz#9009f31bf7be25478895145e92fbc972ad1db900" + integrity sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + source-map-support@^0.5.12: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -2784,12 +2888,17 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -2828,6 +2937,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"