diff --git a/apps/duckguessr/api/package.json b/apps/duckguessr/api/package.json index 8ce1d1d55..72ff088ff 100644 --- a/apps/duckguessr/api/package.json +++ b/apps/duckguessr/api/package.json @@ -25,8 +25,9 @@ "jsonwebtoken": "^9.0.3", "prisma": "^7.10.0", "sharp": "^0.33.5", - "socket-call-server": "^0.9.0", + "socket-call-server": "^0.12.0", "socket.io": "^4.8.3", + "valibot": "^1.5.0", "~api": "workspace:*", "~dm-types": "workspace:*", "~prisma-schemas": "workspace:*" diff --git a/apps/duckguessr/api/pnpm-lock.yaml b/apps/duckguessr/api/pnpm-lock.yaml index 3b25a29e3..892a6bbe6 100644 --- a/apps/duckguessr/api/pnpm-lock.yaml +++ b/apps/duckguessr/api/pnpm-lock.yaml @@ -33,11 +33,14 @@ importers: specifier: ^0.33.5 version: 0.33.5 socket-call-server: - specifier: ^0.9.0 - version: 0.9.0 + specifier: ^0.12.0 + version: 0.12.0(valibot@1.5.0(typescript@6.0.3)) socket.io: specifier: ^4.8.3 version: 4.8.3 + valibot: + specifier: ^1.5.0 + version: 1.5.0(typescript@6.0.3) ~api: specifier: workspace:* version: link:../../../packages/api @@ -1392,9 +1395,14 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} - socket-call-server@0.9.0: - resolution: {integrity: sha512-yh3KIhXlp0jaE5x/XYs+vG5sZjZbsyzTBL9jyJqa2SXJoVhnqjiqk0l6LVtxagpjeMvhTa/UgQYx1L36TShavg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} + peerDependencies: + valibot: ^1.0.0 + peerDependenciesMeta: + valibot: + optional: true socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} @@ -1480,6 +1488,14 @@ packages: typescript: optional: true + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2885,9 +2901,11 @@ snapshots: dependencies: is-arrayish: 0.3.4 - socket-call-server@0.9.0: + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -2985,6 +3003,10 @@ snapshots: optionalDependencies: typescript: 6.0.3 + valibot@1.5.0(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vary@1.1.2: {} which@2.0.2: diff --git a/apps/duckguessr/api/services/datasets.ts b/apps/duckguessr/api/services/datasets.ts index c625b5ce7..a76fd82fd 100644 --- a/apps/duckguessr/api/services/datasets.ts +++ b/apps/duckguessr/api/services/datasets.ts @@ -5,6 +5,8 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import prisma from "../prisma/client"; import namespaces from "./namespaces"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; const listenEvents = () => ({ getDatasets: async () => prisma.$queryRaw< @@ -26,36 +28,34 @@ const listenEvents = () => ({ AND decision = 'ok' GROUP BY dataset.name`, - previewDataset: async ({ - personNationalityFilter, - oldestDateFilterMin: _oldestDateFilterMin, - oldestDateFilterMax: _oldestDateFilterMax, - }: { - personNationalityFilter: string[] | undefined; - oldestDateFilterMin: number | undefined; - oldestDateFilterMax: number | undefined; - }) => { - const errors: string[] = []; - if (personNationalityFilter && !personNationalityFilter.length) { - errors.push( - "At least one nationality is required when using the nationality filter", - ); - } - if (errors.length) { - return { - errors, - datasetSize: 0, - } as const; - } - return prismaCoa.$queryRaw< - [ - { - datasetSize: number; - samples: string; - authors: string; - }, - ] - >` + previewDataset: ev( + v.pipe( + v.object({ + personNationalityFilter: v.optional(v.array(v.string())), + oldestDateFilterMin: v.optional(v.number()), + oldestDateFilterMax: v.optional(v.number()), + }), + v.check( + ({ personNationalityFilter }) => + !personNationalityFilter || personNationalityFilter.length > 0, + "At least one nationality is required when using the nationality filter" as const, + ), + ), + )( + async ({ + personNationalityFilter, + oldestDateFilterMin: _oldestDateFilterMin, + oldestDateFilterMax: _oldestDateFilterMax, + }) => + prismaCoa.$queryRaw< + [ + { + datasetSize: number; + samples: string; + authors: string; + }, + ] + >` with dataset as (select sitecode, url, REPLACE(artsummary, ',', '') as personcode from inducks_entryurl inner join inducks_entry using (entrycode) @@ -89,19 +89,16 @@ const listenEvents = () => ({ ) select datasetSize, samples, authors from dataset_stats, authors_list - `.then( - ([result]) => - ({ - datasetSize: result.datasetSize, - authors: JSON.parse(result.authors), - samples: JSON.parse(result.samples), - }) as { - datasetSize: number; - samples: { url: string; personcode: string }[]; - authors: Record; - }, - ); - }, + `.then<{ + datasetSize: number; + samples: { url: string; personcode: string }[]; + authors: Record; + }>(([result]) => ({ + datasetSize: result.datasetSize, + authors: JSON.parse(result.authors), + samples: JSON.parse(result.samples), + })), + ), }); const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/api/services/game.ts b/apps/duckguessr/api/services/game.ts index 66b579837..385f5a938 100644 --- a/apps/duckguessr/api/services/game.ts +++ b/apps/duckguessr/api/services/game.ts @@ -21,6 +21,8 @@ import namespaces from "./namespaces"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import { io } from ".."; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; export type ClientListenEvents = { playerJoined: (player: player) => void; @@ -342,7 +344,7 @@ const listenEvents = (gameServices: GameServices) => { _socket.broadcast.emit("matchStarts"); gameServices.matchStarts(); }, - guess: async (personcode: string | null) => { + guess: ev(v.nullable(v.string()))(async (personcode) => { const haveAllPlayersGuessed = await onGuess( gameServices, _socket.data.user, @@ -351,8 +353,8 @@ const listenEvents = (gameServices: GameServices) => { if (haveAllPlayersGuessed) { return haveAllPlayersGuessed; } - }, - disconnect: async (reason: string) => { + }), + disconnect: ev(v.string())(async (reason) => { if (reason !== "client namespace disconnect") { if ( _socket && @@ -367,7 +369,7 @@ const listenEvents = (gameServices: GameServices) => { ); } } - }, + }), }; }; diff --git a/apps/duckguessr/api/services/home.ts b/apps/duckguessr/api/services/home.ts index e4948069c..eac880ba9 100644 --- a/apps/duckguessr/api/services/home.ts +++ b/apps/duckguessr/api/services/home.ts @@ -2,6 +2,8 @@ import { useSocketEvents } from "socket-call-server"; import prisma from "../prisma/client"; import namespaces from "./namespaces"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; const convertUrlToBase64 = async (url: string): Promise => { try { @@ -22,7 +24,7 @@ const convertUrlToBase64 = async (url: string): Promise => { }; const listenEvents = () => ({ - getGameRounds: async (gameId: number) => { + getGameRounds: ev(v.number())(async (gameId) => { const round = await prisma.round.findFirst({ include: { roundScores: true, @@ -66,7 +68,7 @@ const listenEvents = () => ({ roundNumber: round.roundNumber, base64, }; - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/api/services/maintenance.ts b/apps/duckguessr/api/services/maintenance.ts index e9b9cd501..75bfa9123 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -1,7 +1,9 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import prisma from "../prisma/client"; -import { type entryurlDetailsDecision } from "../prisma/client_duckguessr/browser"; +import { entryurlDetailsDecision } from "../prisma/client_duckguessr/browser"; import namespaces from "./namespaces"; import { RequiredPlayerMiddleware } from "../middlewares/required-player"; @@ -16,13 +18,13 @@ const listenEvents = () => ({ group by dataset_id, decision `, - getMaintenanceDataForDataset: async ( - datasetName: string, - decisions: (entryurlDetailsDecision | "null")[], - offset: number, - ) => { - if (!decisions) { - throw new Error("No decisions provided"); + getMaintenanceDataForDataset: ev( + v.string(), + v.array(v.enum({ ...entryurlDetailsDecision, null: "null" })), + v.number(), + )(async (datasetName, decisions, offset) => { + if (!decisions.length) { + return []; } const dataset = await prisma.dataset.findUnique({ where: { @@ -53,10 +55,15 @@ const listenEvents = () => ({ sitecodeUrl: "asc", }, }); - }, - updateMaintenanceData: async ( - data: { sitecodeUrl: string; decision: entryurlDetailsDecision }[], - ) => + }), + updateMaintenanceData: ev( + v.array( + v.object({ + sitecodeUrl: v.string(), + decision: v.enum(entryurlDetailsDecision), + }), + ), + )(async (data) => prisma.$transaction( data.map(({ sitecodeUrl, decision }) => prisma.entryurlDetails.update({ @@ -70,6 +77,7 @@ const listenEvents = () => ({ }), ), ), + ), }); const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/api/services/match.ts b/apps/duckguessr/api/services/match.ts index 4854de68b..ace091039 100644 --- a/apps/duckguessr/api/services/match.ts +++ b/apps/duckguessr/api/services/match.ts @@ -8,6 +8,8 @@ import type { SessionUser } from "../types/SessionUser"; import namespaces from "./namespaces"; import { RequiredPlayerMiddleware } from "../middlewares/required-player"; import { createGameSocket } from "./game"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; type MatchServices = NamespaceProxyTarget< Socket< @@ -20,12 +22,12 @@ type MatchServices = NamespaceProxyTarget< >; const listenEvents = ({ _socket }: MatchServices) => ({ - createMatch: async (dataset: string) => { + createMatch: ev(v.string())(async (dataset) => { console.log(`${_socket.data.user.username} is creating a match`); const newGame = (await game.create(dataset))!; await createGameSocket(newGame.id); return newGame.id; - }, + }), }); const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/api/services/player.ts b/apps/duckguessr/api/services/player.ts index eb3167df8..c7201193c 100644 --- a/apps/duckguessr/api/services/player.ts +++ b/apps/duckguessr/api/services/player.ts @@ -7,6 +7,9 @@ import { type player } from "../prisma/client_duckguessr/browser"; import namespaces from "./namespaces"; import { RequiredPlayerMiddleware } from "../middlewares/required-player"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export type ClientListenEvents = { logged: (player: player) => void; loginFailed: () => void; @@ -25,14 +28,13 @@ type PlayerServices = NamespaceProxyTarget< >; const listenEvents = ({ _socket }: PlayerServices) => ({ - getPlayer: () => { - return Promise.resolve(_socket.data.user); - }, + getPlayer: () => Promise.resolve(_socket.data.user), - updateUser: async ({ avatar }: player) => + updateUser: ev(v.object({ avatar: v.string() }))(async ({ avatar }) => updatePlayer(_socket.data.user.id, { avatar }), + ), - getStats: async (gameId?: number) => { + getStats: ev(v.optional(v.number()))(async (gameId) => { const playerIdsToQuery = [_socket.data.user.id]; if (gameId) { playerIdsToQuery.push( @@ -46,9 +48,9 @@ const listenEvents = ({ _socket }: PlayerServices) => ({ ); } return await getPlayerStatistics(playerIdsToQuery); - }, + }), - getGameStats: async (gameId: number) => { + getGameStats: ev(v.number())(async (gameId: number) => { const playerIdsToQuery = [_socket.data.user.id]; if (gameId) { playerIdsToQuery.push( @@ -63,7 +65,7 @@ const listenEvents = ({ _socket }: PlayerServices) => ({ } const stats = await getPlayerStatistics(playerIdsToQuery); return { gameId, stats }; - }, + }), }); const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/app/components/MedalList.vue b/apps/duckguessr/app/components/MedalList.vue index d02b2fae0..178948b22 100644 --- a/apps/duckguessr/app/components/MedalList.vue +++ b/apps/duckguessr/app/components/MedalList.vue @@ -91,68 +91,66 @@ const statsMatchingMedals = computed(() => ?.map(({ medalType }) => medalType), ); -const levelsAndProgress = computed( - (): Record => - !stats.value - ? {} - : MEDAL_LEVELS.reduce((acc, { medalType, levels }) => { - let level = - levels.length - - [...levels].reverse().findIndex( - (levelThreshold: number) => - ( - stats.value!.find( - ({ medalType: statsMedalType }) => - medalType === statsMedalType, - ) || { - points: 0, - } - ).playerPoints! >= levelThreshold, - ); - - if (level === 4) { - level = 0; - } - if (level === 3) { - return { - ...acc, - [medalType]: { - level, - currentLevelPoints: 0, - currentLevelPercentageProgress: 0, - }, - }; - } - const currentLevelThreshold = level === 0 ? 0 : levels[level - 1]; - const currentLevelPoints = - ( - stats.value!.find( - ({ medalType: statsMedalType }) => medalType === statsMedalType, - ) || { - playerPoints: 0, - } - ).playerPoints! - currentLevelThreshold; - const currentLevelProgressPoints = playerStore().gameStats - ? ( - playerStore().gameStats!.stats.find( +const levelsAndProgress = computed((): Record => + !stats.value + ? {} + : MEDAL_LEVELS.reduce((acc, { medalType, levels }) => { + let level = + levels.length - + [...levels].reverse().findIndex( + (levelThreshold: number) => + ( + stats.value!.find( ({ medalType: statsMedalType }) => medalType === statsMedalType, ) || { - playerPoints: 0, + points: 0, } - ).playerPoints - : 0; + ).playerPoints! >= levelThreshold, + ); - const medalLevelAndProgress = { - level, - currentLevelPoints, - currentLevelProgressPoints, - }; + if (level === 4) { + level = 0; + } + if (level === 3) { return { ...acc, - [medalType]: medalLevelAndProgress, + [medalType]: { + level, + currentLevelPoints: 0, + currentLevelPercentageProgress: 0, + }, }; - }, {}), + } + const currentLevelThreshold = level === 0 ? 0 : levels[level - 1]; + const currentLevelPoints = + ( + stats.value!.find( + ({ medalType: statsMedalType }) => medalType === statsMedalType, + ) || { + playerPoints: 0, + } + ).playerPoints! - currentLevelThreshold; + const currentLevelProgressPoints = playerStore().gameStats + ? ( + playerStore().gameStats!.stats.find( + ({ medalType: statsMedalType }) => medalType === statsMedalType, + ) || { + playerPoints: 0, + } + ).playerPoints + : 0; + + const medalLevelAndProgress = { + level, + currentLevelPoints, + currentLevelProgressPoints, + }; + return { + ...acc, + [medalType]: medalLevelAndProgress, + }; + }, {}), ); const noMedalProgress = computed( diff --git a/apps/duckguessr/app/pages/admin/create-dataset.vue b/apps/duckguessr/app/pages/admin/create-dataset.vue index 50d29d2cf..dc3fe1687 100644 --- a/apps/duckguessr/app/pages/admin/create-dataset.vue +++ b/apps/duckguessr/app/pages/admin/create-dataset.vue @@ -116,7 +116,7 @@ -
+
>(); +const datasetPreviewErrors = ref([]); const isCalculatingDatasetPreview = ref(false); @@ -479,6 +477,7 @@ watch( console.log("Filters changed, updating matches..."); isCalculatingDatasetPreview.value = true; datasetPreview.value = undefined; + datasetPreviewErrors.value = []; datasetsSocket .previewDataset({ personNationalityFilter: filters.value.personNationalityFilter, @@ -489,6 +488,9 @@ watch( .then((result) => { datasetPreview.value = result; }) + .catch((error) => { + datasetPreviewErrors.value = [error.error]; + }) .finally(() => { isCalculatingDatasetPreview.value = false; }); diff --git a/apps/duckguessr/app/stores/player.ts b/apps/duckguessr/app/stores/player.ts index 18d50afc9..287328537 100644 --- a/apps/duckguessr/app/stores/player.ts +++ b/apps/duckguessr/app/stores/player.ts @@ -1,5 +1,5 @@ import { defineStore } from "pinia"; -import type { EventOutput } from "socket-call-client"; +import type { SuccessfulEventOutput } from "socket-call-client"; import { duckguessrSocketInjectionKey } from "~/composables/useDuckguessrSocket"; @@ -18,7 +18,8 @@ export const MEDAL_LEVELS: MedalLevel[] = [ export const playerStore = defineStore("player", () => { const playerUser = ref(); const stats = ref(); - const gameStats = ref>(); + const gameStats = + ref>(); const isAnonymous = computed( () => playerUser.value && /^user\d+$/.test(playerUser.value.username), diff --git a/apps/duckguessr/package.json b/apps/duckguessr/package.json index f2961d6ef..5f458c811 100644 --- a/apps/duckguessr/package.json +++ b/apps/duckguessr/package.json @@ -38,7 +38,7 @@ "nuxt": "^4.5.2", "nuxt-typed-router": "4.0.9", "pinia": "^4.0.3", - "socket-call-client": "^0.8.0", + "socket-call-client": "^0.10.0", "universal-cookie": "^8.1.2", "unplugin-vue-components": "^32.1.0", "vue": "^3.5.43", diff --git a/apps/duckguessr/pnpm-lock.yaml b/apps/duckguessr/pnpm-lock.yaml index ae6f68215..d5c3612ac 100644 --- a/apps/duckguessr/pnpm-lock.yaml +++ b/apps/duckguessr/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: ^4.0.3 version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)) socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)) universal-cookie: specifier: ^8.1.2 version: 8.1.2 @@ -6138,8 +6138,8 @@ packages: resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} engines: {node: '>=20.0.0'} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -14009,7 +14009,7 @@ snapshots: smob@1.6.2: {} - socket-call-client@0.8.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.43(typescript@6.0.3) diff --git a/apps/dumili/api/package.json b/apps/dumili/api/package.json index 27f1ade71..284e07487 100644 --- a/apps/dumili/api/package.json +++ b/apps/dumili/api/package.json @@ -41,10 +41,11 @@ "node-unrar-js": "^2.0.2", "pdfjs-dist": "^6.3.289", "sharp": "^0.33.5", - "socket-call-client": "^0.8.0", - "socket-call-server": "^0.9.0", + "socket-call-client": "^0.10.0", + "socket-call-server": "^0.12.0", "socket.io": "^4.8.3", "unpdf": "^1.8.1", + "valibot": "^1.5.0", "~api": "workspace:*", "~prisma-schemas": "workspace:*" }, diff --git a/apps/dumili/api/pnpm-lock.yaml b/apps/dumili/api/pnpm-lock.yaml index fc2c23357..24e67b688 100644 --- a/apps/dumili/api/pnpm-lock.yaml +++ b/apps/dumili/api/pnpm-lock.yaml @@ -60,17 +60,20 @@ importers: specifier: ^0.33.5 version: 0.33.5 socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.22(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.22(typescript@6.0.3)) socket-call-server: - specifier: ^0.9.0 - version: 0.9.0 + specifier: ^0.12.0 + version: 0.12.0(valibot@1.5.0(typescript@6.0.3)) socket.io: specifier: ^4.8.3 version: 4.8.3 unpdf: specifier: ^1.8.1 version: 1.8.1(@napi-rs/canvas@1.0.9) + valibot: + specifier: ^1.5.0 + version: 1.5.0(typescript@6.0.3) ~api: specifier: workspace:* version: link:../../../packages/api @@ -1962,8 +1965,8 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -1972,9 +1975,14 @@ packages: axios-cache-interceptor: optional: true - socket-call-server@0.9.0: - resolution: {integrity: sha512-yh3KIhXlp0jaE5x/XYs+vG5sZjZbsyzTBL9jyJqa2SXJoVhnqjiqk0l6LVtxagpjeMvhTa/UgQYx1L36TShavg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} + peerDependencies: + valibot: ^1.0.0 + peerDependenciesMeta: + valibot: + optional: true socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} @@ -2093,6 +2101,14 @@ packages: typescript: optional: true + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -4040,7 +4056,7 @@ snapshots: dependencies: is-arrayish: 0.3.4 - socket-call-client@0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.22(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.22(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.22(typescript@6.0.3) @@ -4051,9 +4067,11 @@ snapshots: - supports-color - utf-8-validate - socket-call-server@0.9.0: + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -4176,6 +4194,10 @@ snapshots: optionalDependencies: typescript: 6.0.3 + valibot@1.5.0(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vary@1.1.2: {} vue@3.5.22(typescript@6.0.3): diff --git a/apps/dumili/api/services/indexation-creation/index.ts b/apps/dumili/api/services/indexation-creation/index.ts index bc7de87d6..e0326beb3 100644 --- a/apps/dumili/api/services/indexation-creation/index.ts +++ b/apps/dumili/api/services/indexation-creation/index.ts @@ -10,20 +10,26 @@ import { OptionalAuthMiddleware } from "../_auth"; import { createEntry } from "../indexation"; import namespaces from "../namespaces"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + type IndexationCreationServices = NamespaceProxyTarget< Socket, Record >; const listenEvents = ({ _socket }: IndexationCreationServices) => ({ - create: async (numberOfPages: number) => { - if (numberOfPages < 4 || numberOfPages > 996 || numberOfPages % 2 !== 0) { - return { - error: `Invalid number of pages`, - errorDetails: JSON.stringify({ numberOfPages }), - } as const; - } - return prisma.indexation + create: ev( + v.pipe( + v.number(), + v.check( + (numberOfPages) => + numberOfPages >= 4 && numberOfPages <= 996 && numberOfPages % 2 === 0, + "Invalid number of pages" as const, + ), + ), + )(async (numberOfPages) => + prisma.indexation .create({ data: { dmUserId: _socket.data.user?.id || null, @@ -49,8 +55,8 @@ const listenEvents = ({ _socket }: IndexationCreationServices) => ({ }, }), ) - .then((entry) => entry.indexationId); - }, + .then((entry) => entry.indexationId), + ), }); const { client, server } = useSocketEvents< diff --git a/apps/dumili/api/services/indexation/index.ts b/apps/dumili/api/services/indexation/index.ts index 31c10ecce..29d807206 100644 --- a/apps/dumili/api/services/indexation/index.ts +++ b/apps/dumili/api/services/indexation/index.ts @@ -14,13 +14,8 @@ import { getEntryPages } from "~dumili-utils/entryPages"; import prisma from "~prisma/client"; import type { aiKumikoResult, - entry, - indexation, - issueSuggestion, page, Prisma, - storyKindSuggestion, - storySuggestion, } from "~prisma/client_dumili/client"; import type { ClientEvents as CoaEvents } from "~dm-services/coa"; import dmNamespaces from "~dm-services/namespaces"; @@ -45,6 +40,9 @@ import { definePDFJSModule, getDocumentProxy, renderPageAsImage } from "unpdf"; import { createExtractorFromData } from "node-unrar-js"; import { unzipSync } from "fflate"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + await definePDFJSModule(() => import("pdfjs-dist")); // In the production bundle, bun bakes in the CI build path for unrar.wasm. @@ -921,8 +919,10 @@ export const handleHttpFileUpload = async ( }; const listenEvents = (services: IndexationServices) => ({ - setPageUrl: async (id: number, url: string | null) => - setPageUrl(services, id, url), + setPageUrl: ev( + v.number(), + v.nullable(v.string()), + )(async (id, url) => setPageUrl(services, id, url)), deleteIndexation: async () => { const { id: indexationId } = services._socket.data.indexation; @@ -938,7 +938,7 @@ const listenEvents = (services: IndexationServices) => ({ return { indexation: services._socket.data.indexation }; }, - deleteEntry: async (entryId: entry["id"]) => { + deleteEntry: ev(v.number())(async (entryId) => { const { indexation } = services._socket.data; const entry = indexation.entries.find(({ id }) => id === entryId); if (!entry) { @@ -960,9 +960,12 @@ const listenEvents = (services: IndexationServices) => ({ await refreshIndexation(services); return { status: "OK" }; - }, + }), - swapPageUrls: async (pageNumber1: number, pageNumber2: number) => + swapPageUrls: ev( + v.number(), + v.number(), + )(async (pageNumber1, pageNumber2) => // In 2 steps so that we don't have to deal with unique constraints prisma.indexation .update({ @@ -1031,8 +1034,9 @@ const listenEvents = (services: IndexationServices) => ({ status: "OK" as const, }; }), + ), - acceptIssueSuggestion: async (suggestionId: issueSuggestion["id"] | null) => { + acceptIssueSuggestion: ev(v.nullable(v.number()))(async (suggestionId) => { if ( !services._socket.data.indexation.issueSuggestions.some( ({ id }) => id === suggestionId, @@ -1065,11 +1069,14 @@ const listenEvents = (services: IndexationServices) => ({ status: "OK", }; }); - }, + }), - createStorySuggestion: async ( - suggestion: Prisma.storySuggestionUncheckedCreateInput, - ) => + createStorySuggestion: ev( + v.object({ + storycode: v.string(), + entryId: v.number(), + }), + )(async (suggestion) => prisma.storySuggestion .create({ data: suggestion, @@ -1080,13 +1087,14 @@ const listenEvents = (services: IndexationServices) => ({ createdStorySuggestion, }; }), + ), - createIssueSuggestion: async ( - suggestion: Omit< - Prisma.issueSuggestionUncheckedCreateInput, - "indexationId" - >, - ) => + createIssueSuggestion: ev( + v.object({ + publicationcode: v.string(), + issuenumber: v.string(), + }), + )(async (suggestion) => prisma.issueSuggestion .create({ data: { @@ -1124,25 +1132,29 @@ const listenEvents = (services: IndexationServices) => ({ return createdIssueSuggestion; }), ), - - updateIndexation: async ( - indexation: Pick & { - numberOfPages: number; - }, - ) => { + ), + + updateIndexation: ev( + v.pipe( + v.object({ + price: v.nullable(v.string()), + releaseDate: v.nullable(v.string()), + title: v.nullable(v.string()), + numberOfPages: v.number(), + }), + v.check( + ({ numberOfPages }) => + numberOfPages >= 4 && numberOfPages <= 996 && numberOfPages % 2 === 0, + "Invalid number of pages" as const, + ), + v.check( + ({ releaseDate }) => + !releaseDate || !Number.isNaN(new Date(releaseDate).getTime()), + "Invalid release date" as const, + ), + ), + )(async (indexation) => { const { numberOfPages, ...changes } = indexation; - if (changes.releaseDate && !new Date(changes.releaseDate)) { - return { - error: `Invalid release date`, - errorDetails: JSON.stringify({ releaseDate: changes.releaseDate }), - }; - } - if (numberOfPages < 4 || numberOfPages > 996 || numberOfPages % 2 !== 0) { - return { - error: `Invalid number of pages`, - errorDetails: JSON.stringify({ numberOfPages }), - }; - } const currentMaxPageNumber = Math.max( ...services._socket.data.indexation.pages.map( ({ pageNumber }) => pageNumber, @@ -1183,12 +1195,12 @@ const listenEvents = (services: IndexationServices) => ({ .then(() => ({ status: "OK", })); - }, + }), - acceptStorySuggestion: async ( - entryId: entry["id"], - storySuggestionId: storySuggestion["id"] | null, - ) => { + acceptStorySuggestion: ev( + v.number(), + v.nullable(v.number()), + )(async (entryId, storySuggestionId) => { const entry = services._socket.data.indexation.entries.find( ({ id, storySuggestions }) => (entryId === id && storySuggestionId === null) || @@ -1212,12 +1224,12 @@ const listenEvents = (services: IndexationServices) => ({ await refreshIndexation(services); return { status: "OK" }; - }, + }), - acceptStoryKindSuggestion: async ( - entryId: entry["id"], - storyKindSuggestionId: storyKindSuggestion["id"] | null, - ) => { + acceptStoryKindSuggestion: ev( + v.number(), + v.nullable(v.number()), + )(async (entryId, storyKindSuggestionId) => { const entry = services._socket.data.indexation.entries.find( ({ id }) => id === entryId, ); @@ -1249,12 +1261,16 @@ const listenEvents = (services: IndexationServices) => ({ await refreshIndexation(services); return { status: "OK" }; - }, - - updateEntry: async ( - entryId: entry["id"], - data: Partial>, - ) => { + }), + + updateEntry: ev( + v.number(), + v.object({ + entirepages: v.optional(v.number()), + title: v.nullish(v.string()), + position: v.optional(v.number()), + }), + )(async (entryId, data) => { const entry = services._socket.data.indexation.entries.find( ({ id }) => id === entryId, ); @@ -1275,12 +1291,12 @@ const listenEvents = (services: IndexationServices) => ({ await refreshIndexation(services); return { status: "OK" }; - }, + }), - createEntry: ( - position: number, - includedInEntryId: number | undefined = undefined, - ) => + createEntry: ev( + v.number(), + v.optional(v.number()), + )((position, includedInEntryId = undefined) => createEntry( services._socket.data.indexation.id, position, @@ -1296,6 +1312,7 @@ const listenEvents = (services: IndexationServices) => ({ status: "OK", }; }), + ), }); export const { client, server } = useSocketEvents< diff --git a/apps/dumili/package.json b/apps/dumili/package.json index 068c3a728..ddf8fbbef 100644 --- a/apps/dumili/package.json +++ b/apps/dumili/package.json @@ -43,7 +43,7 @@ "bootstrap-vue-next": "^1.2.1", "js-cookie": "^3.0.8", "pinia": "^4.0.3", - "socket-call-client": "^0.8.0", + "socket-call-client": "^0.10.0", "sortablejs": "^1.15.7", "vue": "^3.5.43", "vue-draggable-resizable": "^3.0.0", diff --git a/apps/dumili/pnpm-lock.yaml b/apps/dumili/pnpm-lock.yaml index 6f7fda8c3..ffe48b88f 100644 --- a/apps/dumili/pnpm-lock.yaml +++ b/apps/dumili/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^4.0.3 version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)) socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)) sortablejs: specifier: ^1.15.7 version: 1.15.7 @@ -4482,8 +4482,8 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -9598,7 +9598,7 @@ snapshots: slash@5.1.0: {} - socket-call-client@0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.43(typescript@6.0.3) diff --git a/apps/dumili/src/pages/index.vue b/apps/dumili/src/pages/index.vue index f44ab368e..cf49e02b0 100644 --- a/apps/dumili/src/pages/index.vue +++ b/apps/dumili/src/pages/index.vue @@ -86,7 +86,7 @@ ) }} = 0.4'} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -1495,9 +1498,14 @@ packages: axios-cache-interceptor: optional: true - socket-call-server@0.9.0: - resolution: {integrity: sha512-yh3KIhXlp0jaE5x/XYs+vG5sZjZbsyzTBL9jyJqa2SXJoVhnqjiqk0l6LVtxagpjeMvhTa/UgQYx1L36TShavg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} + peerDependencies: + valibot: ^1.0.0 + peerDependenciesMeta: + valibot: + optional: true socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} @@ -1621,6 +1629,14 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3174,7 +3190,7 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - socket-call-client@0.8.0(vue@3.5.22(typescript@6.0.3)): + socket-call-client@0.10.0(vue@3.5.22(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.22(typescript@6.0.3) @@ -3183,9 +3199,11 @@ snapshots: - supports-color - utf-8-validate - socket-call-server@0.9.0: + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -3330,6 +3348,10 @@ snapshots: utils-merge@1.0.1: {} + valibot@1.5.0(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vary@1.1.2: {} vue@3.5.22(typescript@6.0.3): diff --git a/apps/edgecreator/api/services/browse/index.ts b/apps/edgecreator/api/services/browse/index.ts index 342393489..bce152038 100644 --- a/apps/edgecreator/api/services/browse/index.ts +++ b/apps/edgecreator/api/services/browse/index.ts @@ -4,6 +4,8 @@ import path from "path"; import type { Socket } from "socket.io"; import type { NamespaceProxyTarget } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { OptionalAuthMiddleware } from "~dm-services/auth/util"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; @@ -192,28 +194,30 @@ const findOngoingEdges = async (currentUsername?: string) => { }; const listenEvents = (services: BrowseServices) => ({ - listPublishedEdgeModels: async ( - publicationcode: string, - ): Promise< - | { - error: "Generic error"; - errorDetails: string; - } - | { results: Awaited> } - > => - new Promise((resolve) => { - findPublishedEdges(publicationcode) - .then((results) => { - resolve({ results }); - }) - .catch((errorDetails) => { - console.error(errorDetails); - return resolve({ - error: "Generic error", - errorDetails: errorDetails as string, - } as const); - }); - }), + listPublishedEdgeModels: ev(v.string())( + async ( + publicationcode, + ): Promise< + | { + error: "Generic error"; + errorDetails: string; + } + | { results: Awaited> } + > => + new Promise((resolve) => { + findPublishedEdges(publicationcode) + .then((results) => { + resolve({ results }); + }) + .catch((errorDetails) => { + console.error(errorDetails); + return resolve({ + error: "Generic error", + errorDetails: errorDetails as string, + } as const); + }); + }), + ), listOngoingEdgeModels: async (): Promise< | { error: "Generic error"; @@ -235,19 +239,16 @@ const listenEvents = (services: BrowseServices) => ({ }); }), - listEdgeParts: async (parameters: { - imageType: "elements" | "photos"; - country: string; - magazine: string; - }) => { - const { country, imageType, magazine } = parameters; - if ( - !/^(elements)|(photos)$/.test(imageType) || - !/^[a-z]+$/.test(country) || - !/^[-A-Z0-9]+$/.test(magazine) - ) { - return { error: "Invalid parameters" }; - } + listEdgeParts: ev( + v.object({ + imageType: v.union( + [v.literal("elements"), v.literal("photos")], + "Invalid image type", + ), + country: v.pipe(v.string(), v.regex(/^[a-z]+$/, "Invalid country")), + magazine: v.pipe(v.string(), v.regex(/^[-A-Z0-9]+$/, "Invalid magazine")), + }), + )(async ({ country, imageType, magazine }) => { try { return { results: readdirSync( @@ -259,7 +260,7 @@ const listenEvents = (services: BrowseServices) => ({ } catch (_e) { return { results: [] }; } - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/apps/edgecreator/api/services/image-info/index.ts b/apps/edgecreator/api/services/image-info/index.ts index 192739548..fd4217120 100644 --- a/apps/edgecreator/api/services/image-info/index.ts +++ b/apps/edgecreator/api/services/image-info/index.ts @@ -2,8 +2,11 @@ import axios from "axios"; import sizeOf from "image-size"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + const listenEvents = () => ({ - getImageInfo: async (targetUrl: string) => { + getImageInfo: ev(v.string())(async (targetUrl) => { const url = targetUrl.startsWith("https://res.cloudinary.com") ? targetUrl : `${process.env.EDGES_URL!}/${targetUrl}`; @@ -31,7 +34,7 @@ const listenEvents = () => ({ error: "Cloudinary error", errorDetails: `${targetUrl} : HTTP ${response.status}`, }; - }, + }), }); export const { client, server } = useSocketEvents( diff --git a/apps/edgecreator/api/services/legacy/index.ts b/apps/edgecreator/api/services/legacy/index.ts index cd19d2f1c..d604365ba 100644 --- a/apps/edgecreator/api/services/legacy/index.ts +++ b/apps/edgecreator/api/services/legacy/index.ts @@ -1,9 +1,18 @@ import { useSocketEvents } from "socket-call-server"; import { prismaClient as prismaEdgeCreator } from "~prisma-schemas/schemas/edgecreator/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + const listenEvents = () => ({ - getLegacyDimensions: async (publicationcode: string, issuenumber: string) => - prismaEdgeCreator.$queryRaw<{ optionName: string; optionValue: string }[]>` + getLegacyDimensions: ev( + v.string(), + v.string(), + )( + async (publicationcode, issuenumber) => + prismaEdgeCreator.$queryRaw< + { optionName: string; optionValue: string }[] + >` with inducks_issues as (select row_number() over () as row_num, issuenumber from coa.inducks_issue where publicationcode = ${publicationcode} order by issuecode) @@ -17,17 +26,21 @@ const listenEvents = () => ({ and (select inducks_issues.row_num from inducks_issues where issuenumber = ${issuenumber}) between (select inducks_issues.row_num from inducks_issues where issuenumber = Numero_debut) and (select inducks_issues.row_num from inducks_issues where issuenumber = Numero_fin)`, - getLegacySteps: async ( - publicationcode: string, - issuenumber: string, - ) => prismaEdgeCreator.$queryRaw< - { - stepNumber: number; - functionName: string; - optionName: string; - optionValue: string; - }[] - >` + ), + + getLegacySteps: ev( + v.string(), + v.string(), + )( + async (publicationcode, issuenumber) => + prismaEdgeCreator.$queryRaw< + { + stepNumber: number; + functionName: string; + optionName: string; + optionValue: string; + }[] + >` with inducks_issues as (select row_number() over (order by issuecode) as row_num, issuenumber from coa.inducks_issue where publicationcode = ${publicationcode}) @@ -52,6 +65,7 @@ const listenEvents = () => ({ where issuenumber = Numero_fin) group by Pays, Magazine, Ordre, Option_nom`, + ), }); export const { client, server } = useSocketEvents< typeof listenEvents, diff --git a/apps/edgecreator/api/services/save/index.ts b/apps/edgecreator/api/services/save/index.ts index 5bbfabab0..cf93d88a6 100644 --- a/apps/edgecreator/api/services/save/index.ts +++ b/apps/edgecreator/api/services/save/index.ts @@ -5,11 +5,12 @@ import type { Socket } from "socket.io"; import { SocketClient } from "socket-call-client"; import type { NamespaceProxyTarget } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { type ClientEvents as EdgeCreatorEvents } from "~dm-services/edgecreator"; import namespaces from "~dm-services/namespaces"; import type { ExportPaths } from "~types/ExportPaths"; -import type { ModelContributor } from "~types/ModelContributor"; import { getSvgPath } from "../../_utils"; @@ -33,16 +34,23 @@ const getEdgeCreatorServices = (token: string) => { }; const listenEvents = (services: SaveServices) => ({ - saveEdge: async (parameters: { - runExport: boolean; - runSubmit: boolean; - issuecode: string; - contributors: ModelContributor[]; - content: string; - }) => { + saveEdge: ev( + v.object({ + runExport: v.boolean(), + runSubmit: v.boolean(), + issuecode: v.string(), + contributors: v.array( + v.object({ + contributionType: v.string(), + user: v.object({ + username: v.string(), + }), + }), + ), + content: v.string(), + }), + )(async ({ runExport, runSubmit, issuecode, contributors, content }) => { const { token } = services._socket.handshake.auth; - const { runExport, runSubmit, issuecode, contributors, content } = - parameters; const svgPath = await getSvgPath(runExport, issuecode); mkdirSync(path.dirname(svgPath), { recursive: true }); @@ -96,7 +104,7 @@ const listenEvents = (services: SaveServices) => ({ } return { results: { paths, isNew: false } }; } - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/apps/edgecreator/api/services/text/index.ts b/apps/edgecreator/api/services/text/index.ts index 9d4748c57..1b464d82c 100644 --- a/apps/edgecreator/api/services/text/index.ts +++ b/apps/edgecreator/api/services/text/index.ts @@ -3,6 +3,8 @@ import { promisify } from "node:util"; import { v2 as cloudinary } from "cloudinary"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; const execFileAsync = promisify(execFile); @@ -80,78 +82,93 @@ const generateImage = (parameters: { }); const listenEvents = () => ({ - getText: (parameters: { - color: string; - colorBackground: string; - width: number; - font: string; - text: string; - }): Promise< - | { - error: "Image generation error"; - errorDetails: string; - } - | { results: { width: number; height: number; url: string } } - > => - new Promise((resolve) => { - const { color, colorBackground, width, font, text } = parameters; - const context: Record = { - color, - colorBackground, - width, - text, - }; - cloudinary.search - .expression( - `tags=${font} AND ${Object.keys(context) - .reduce( - (acc, key) => [ - ...acc, - `context.${key}="${String(context[key])}"`, - ], - [], - ) - .join(" AND ")}`, - ) - .execute() - .then( - ({ - resources, - }: { - resources: { - width: number; - height: number; - secure_url: string; - }[]; - }) => { - if (resources.length) { - console.log(`Found an existing text`); - const { width, height, secure_url: url } = resources[0]; - resolve({ results: { width, height, url } }); - } else { - console.log(`Found no existing text, generating text image...`); - generateImage(parameters) - .then(({ width, height, secure_url: url }) => { - console.log(`Text image generated: url=${url}`); - resolve({ results: { width, height, url } }); + getText: ev( + v.object({ + color: v.string(), + colorBackground: v.string(), + width: v.number(), + font: v.string(), + text: v.string(), + }), + )( + async ({ + color, + colorBackground, + width, + font, + text, + }): Promise< + | { + error: "Image generation error"; + errorDetails: string; + } + | { results: { width: number; height: number; url: string } } + > => + new Promise((resolve) => { + const context: Record = { + color, + colorBackground, + width, + text, + }; + cloudinary.search + .expression( + `tags=${font} AND ${Object.keys(context) + .reduce( + (acc, key) => [ + ...acc, + `context.${key}="${String(context[key])}"`, + ], + [], + ) + .join(" AND ")}`, + ) + .execute() + .then( + ({ + resources, + }: { + resources: { + width: number; + height: number; + secure_url: string; + }[]; + }) => { + if (resources.length) { + console.log(`Found an existing text`); + const { width, height, secure_url: url } = resources[0]; + resolve({ results: { width, height, url } }); + } else { + console.log(`Found no existing text, generating text image...`); + generateImage({ + color, + colorBackground, + width, + font, + text, }) - .catch((response: Error) => { - resolve({ - error: "Image generation error", - errorDetails: response.message, + .then(({ width, height, secure_url: url }) => { + console.log(`Text image generated: url=${url}`); + resolve({ results: { width, height, url } }); + }) + .catch((response: Error) => { + resolve({ + error: "Image generation error", + errorDetails: response.message, + }); }); - }); - } - }, - ) - .catch((e) => { - console.error(e); - resolve({ - error: "Image generation error", - errorDetails: e.message, + } + }, + ) + .catch((e) => { + console.error(e); + resolve({ + error: "Image generation error", + errorDetails: e.message, + }); }); - }); - }), + }), + ), }); export const { client, server } = useSocketEvents< diff --git a/apps/edgecreator/api/services/upload/index.ts b/apps/edgecreator/api/services/upload/index.ts index b0ef0ad8a..a2ed90bd7 100644 --- a/apps/edgecreator/api/services/upload/index.ts +++ b/apps/edgecreator/api/services/upload/index.ts @@ -6,6 +6,8 @@ import type { Socket } from "socket.io"; import { SocketClient } from "socket-call-client"; import type { NamespaceProxyTarget } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { RequiredAuthMiddleware } from "~dm-services/auth/util"; import type { ClientEvents as EdgeCreatorServices } from "~dm-services/edgecreator"; @@ -142,30 +144,33 @@ type UploadServices = NamespaceProxyTarget< >; const listenEvents = ({ _socket: socket }: UploadServices) => ({ - uploadFromBase64: async ( - parameters: { - data: string; - issuecode: string; - } & ( - | { - isEdgePhoto: false; - fileName: string; - } - | { - isEdgePhoto: true; - fileName?: undefined; - } - ), - ) => { - const { issuecode, data, isEdgePhoto, fileName } = parameters; - if (!isEdgePhoto && (/[/\\]/.test(fileName) || fileName.includes(".."))) { - return { error: "Invalid file name" } as const; - } + uploadFromBase64: ev( + v.union([ + v.object({ + data: v.string(), + issuecode: v.string(), + isEdgePhoto: v.literal(false), + fileName: v.pipe( + v.string(), + v.check( + (fileName) => !/[/\\]/.test(fileName) && !fileName.includes(".."), + "Invalid file name" as const, + ), + ), + }), + v.object({ + data: v.string(), + issuecode: v.string(), + isEdgePhoto: v.literal(true), + }), + ]), + )(async (input) => { + const { issuecode, data, isEdgePhoto } = input; const cleanData = data.includes(",") ? data.split(",")[1] : data; const targetFilePath = await getTargetFilePath( isEdgePhoto ? { issuecode, isEdgePhoto } - : { issuecode, isEdgePhoto, fileName }, + : { issuecode, isEdgePhoto, fileName: input.fileName }, ); const token = socket.data.user!.token; @@ -200,7 +205,7 @@ const listenEvents = ({ _socket: socket }: UploadServices) => ({ } return { fileName: targetFileName }; - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/apps/edgecreator/package.json b/apps/edgecreator/package.json index 16f58dc4b..d742042de 100644 --- a/apps/edgecreator/package.json +++ b/apps/edgecreator/package.json @@ -38,7 +38,7 @@ "js-cookie": "^3.0.8", "pinia": "^4.0.3", "sass": "^1.105.0", - "socket-call-client": "^0.8.0", + "socket-call-client": "^0.10.0", "universal-cookie": "^7.2.2", "vite-multiple-assets": "^2.2.6", "vue": "^3.5.43", diff --git a/apps/edgecreator/pnpm-lock.yaml b/apps/edgecreator/pnpm-lock.yaml index 0eb0de3e2..3737a2bde 100644 --- a/apps/edgecreator/pnpm-lock.yaml +++ b/apps/edgecreator/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ^1.105.0 version: 1.105.0 socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)) universal-cookie: specifier: ^7.2.2 version: 7.2.2 @@ -2787,8 +2787,8 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -5895,7 +5895,7 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - socket-call-client@0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.43(typescript@6.0.3) diff --git a/apps/edgecreator/src/composables/useLegacyDb.ts b/apps/edgecreator/src/composables/useLegacyDb.ts index 62083cc77..e31125eb5 100644 --- a/apps/edgecreator/src/composables/useLegacyDb.ts +++ b/apps/edgecreator/src/composables/useLegacyDb.ts @@ -2,7 +2,7 @@ import type { EdgeDimensions } from "~/types/EdgeDimensions"; import type { LegacyComponent } from "~/types/LegacyComponent"; import type { StepOptions } from "~/types/StepOptions"; import type { ClientEvents as EdgeCreatorClientEvents } from "~edgecreator-services/image-info"; -import type { EventOutput } from "socket-call-client"; +import type { SuccessfulEventOutput } from "socket-call-client"; import useTextTemplate from "~/composables/useTextTemplate"; const { resolveIssueNumberTemplate } = useTextTemplate(); @@ -44,7 +44,9 @@ export default () => { edgeDimensions: EdgeDimensions, getImageInfoFn: ( elementPath: string, - ) => Promise>, + ) => Promise< + SuccessfulEventOutput + >, calculateBase64: boolean, ) => { switch (targetComponent) { @@ -134,14 +136,18 @@ export default () => { let image; if (calculateBase64) { - image = await getImageInfoFn(elementPath); - if ("errorDetails" in image) { + try { + image = (await getImageInfoFn(elementPath)).results; + } catch (error) { console.error( - `Image could not be retrieved : ${image.errorDetails}`, + `Image could not be retrieved : ${ + typeof error === "object" && error && "errorDetails" in error + ? String(error.errorDetails) + : String(error) + }`, ); return; } - image = image.results!; } else { image = { dimensions: await getImageSize( diff --git a/apps/inducksql/pnpm-lock.yaml b/apps/inducksql/pnpm-lock.yaml index 7b00c3301..b6ce3d891 100644 --- a/apps/inducksql/pnpm-lock.yaml +++ b/apps/inducksql/pnpm-lock.yaml @@ -95,15 +95,6 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -184,105 +175,99 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@napi-rs/wasm-runtime@1.2.4': - resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - - '@oxc-project/types@0.130.0': - resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@oxc-project/types@0.151.0': + resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} '@pkgr/core@0.1.2': resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@rolldown/binding-android-arm64@1.0.1': - resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + '@rolldown/binding-android-arm-eabi@1.2.11': + resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.11': + resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.1': - resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + '@rolldown/binding-darwin-arm64@1.2.11': + resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.1': - resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + '@rolldown/binding-darwin-x64@1.2.11': + resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.1': - resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + '@rolldown/binding-freebsd-x64@1.2.11': + resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': - resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.1': - resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + '@rolldown/binding-linux-arm64-gnu@1.2.11': + resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.1': - resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + '@rolldown/binding-linux-arm64-musl@1.2.11': + resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.1': - resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.1': - resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.11': + resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.1': - resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + '@rolldown/binding-linux-x64-gnu@1.2.11': + resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.1': - resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + '@rolldown/binding-linux-x64-musl@1.2.11': + resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.1': - resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + '@rolldown/binding-openharmony-arm64@1.2.11': + resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.1': - resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.1': - resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + '@rolldown/binding-win32-arm64-msvc@1.2.11': + resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.1': - resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + '@rolldown/binding-win32-x64-msvc@1.2.11': + resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -294,9 +279,6 @@ packages: resolution: {integrity: sha512-U8pvL0patYHMgieD8JxxYJTlh9m1LUX8+BDYb7MiKQzBI89f2qbdmdcG/PvnPaLm9wsBbv4VlVG453ixnxBOnA==} engines: {node: '>=22'} - '@tybys/wasm-util@0.10.4': - resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} - '@types/bun@1.3.14': resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} @@ -875,8 +857,8 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - rolldown@1.0.1: - resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + rolldown@1.2.11: + resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1095,22 +1077,6 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.11.0)': dependencies: eslint: 10.11.0 @@ -1177,75 +1143,59 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.4 - optional: true - - '@oxc-project/types@0.130.0': {} + '@oxc-project/types@0.151.0': {} '@pkgr/core@0.1.2': {} - '@rolldown/binding-android-arm64@1.0.1': + '@rolldown/binding-android-arm-eabi@1.2.11': optional: true - '@rolldown/binding-darwin-arm64@1.0.1': + '@rolldown/binding-android-arm64@1.2.11': optional: true - '@rolldown/binding-darwin-x64@1.0.1': + '@rolldown/binding-darwin-arm64@1.2.11': optional: true - '@rolldown/binding-freebsd-x64@1.0.1': + '@rolldown/binding-darwin-x64@1.2.11': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + '@rolldown/binding-freebsd-x64@1.2.11': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.1': + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.1': + '@rolldown/binding-linux-arm64-gnu@1.2.11': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.1': + '@rolldown/binding-linux-arm64-musl@1.2.11': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.1': + '@rolldown/binding-linux-ppc64-gnu@1.2.11': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.1': + '@rolldown/binding-linux-s390x-gnu@1.2.11': optional: true - '@rolldown/binding-linux-x64-musl@1.0.1': + '@rolldown/binding-linux-x64-gnu@1.2.11': optional: true - '@rolldown/binding-openharmony-arm64@1.0.1': + '@rolldown/binding-linux-x64-musl@1.2.11': optional: true - '@rolldown/binding-wasm32-wasi@1.0.1': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@rolldown/binding-openharmony-arm64@1.2.11': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.1': + '@rolldown/binding-win32-arm64-msvc@1.2.11': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.1': + '@rolldown/binding-win32-x64-msvc@1.2.11': optional: true '@rolldown/pluginutils@1.0.1': {} '@sqlite.org/sqlite-wasm@3.53.4-build1': {} - '@tybys/wasm-util@0.10.4': - dependencies: - tslib: 2.8.1 - optional: true - '@types/bun@1.3.14': dependencies: bun-types: 1.3.14 @@ -1841,26 +1791,26 @@ snapshots: require-directory@2.1.1: {} - rolldown@1.0.1: + rolldown@1.2.11: dependencies: - '@oxc-project/types': 0.130.0 + '@oxc-project/types': 0.151.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.1 - '@rolldown/binding-darwin-arm64': 1.0.1 - '@rolldown/binding-darwin-x64': 1.0.1 - '@rolldown/binding-freebsd-x64': 1.0.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 - '@rolldown/binding-linux-arm64-gnu': 1.0.1 - '@rolldown/binding-linux-arm64-musl': 1.0.1 - '@rolldown/binding-linux-ppc64-gnu': 1.0.1 - '@rolldown/binding-linux-s390x-gnu': 1.0.1 - '@rolldown/binding-linux-x64-gnu': 1.0.1 - '@rolldown/binding-linux-x64-musl': 1.0.1 - '@rolldown/binding-openharmony-arm64': 1.0.1 - '@rolldown/binding-wasm32-wasi': 1.0.1 - '@rolldown/binding-win32-arm64-msvc': 1.0.1 - '@rolldown/binding-win32-x64-msvc': 1.0.1 + '@rolldown/binding-android-arm-eabi': 1.2.11 + '@rolldown/binding-android-arm64': 1.2.11 + '@rolldown/binding-darwin-arm64': 1.2.11 + '@rolldown/binding-darwin-x64': 1.2.11 + '@rolldown/binding-freebsd-x64': 1.2.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 + '@rolldown/binding-linux-arm64-gnu': 1.2.11 + '@rolldown/binding-linux-arm64-musl': 1.2.11 + '@rolldown/binding-linux-ppc64-gnu': 1.2.11 + '@rolldown/binding-linux-s390x-gnu': 1.2.11 + '@rolldown/binding-linux-x64-gnu': 1.2.11 + '@rolldown/binding-linux-x64-musl': 1.2.11 + '@rolldown/binding-openharmony-arm64': 1.2.11 + '@rolldown/binding-win32-arm64-msvc': 1.2.11 + '@rolldown/binding-win32-x64-msvc': 1.2.11 rxjs@7.8.2: dependencies: @@ -1946,7 +1896,7 @@ snapshots: lightningcss: 1.33.0 picomatch: 4.0.7 postcss: 8.5.28 - rolldown: 1.0.1 + rolldown: 1.2.11 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.6.2 diff --git a/apps/web/package.json b/apps/web/package.json index 670f491f0..fa497bfa4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -35,7 +35,7 @@ "mapbox-gl": "^3.31.0", "page-flip": "^2.0.7", "pinia": "^4.0.3", - "socket-call-client": "^0.8.0", + "socket-call-client": "^0.10.0", "timeago.js": "^4.0.2", "v-contextmenu": "^3.2.0", "vue": "^3.5.43", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 6d8eccd84..8cb6af7f6 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ^4.0.3 version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)) socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)) timeago.js: specifier: ^4.0.2 version: 4.0.2 @@ -3345,8 +3345,8 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -7146,7 +7146,7 @@ snapshots: slash@5.1.0: {} - socket-call-client@0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.43(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.43(typescript@6.0.3) diff --git a/apps/web/src/pages/image-search-test.vue b/apps/web/src/pages/image-search-test.vue index c4625e87c..d74400a73 100644 --- a/apps/web/src/pages/image-search-test.vue +++ b/apps/web/src/pages/image-search-test.vue @@ -204,7 +204,7 @@ const models = ref< results?: Results; }[] >([ - ...[0, 1].map( + ...([0, 1] as const).map( (pastecIndex) => ({ isSelected: true, diff --git a/apps/web/src/stores/coa.ts b/apps/web/src/stores/coa.ts index 5db99575d..b762ce8d9 100644 --- a/apps/web/src/stores/coa.ts +++ b/apps/web/src/stores/coa.ts @@ -4,7 +4,6 @@ import type { ClientEvents as CoaClientEvents } from "~dm-services/coa"; import type { InducksIssueDetails } from "~dm-types/InducksIssueDetails"; import type { InducksIssueQuotationSimple } from "~dm-types/InducksIssueQuotationSimple"; import type { - ExtraSelectField, inducks_story, inducks_storyversion, IssuecodeDetail, @@ -54,12 +53,17 @@ export const coa = defineStore("coa", () => { const locale = useI18n().locale, coverUrls = shallowRef<{ [issuecode: string]: string }>({}), - countryNames = shallowRef>(), + countryNames = + shallowRef>(), publicationNames = shallowRef< - EventOutput + SuccessfulEventOutput< + CoaClientEvents, + "getPublicationListFromCountrycodes" + > >({}), publicationNamesFullCountries = shallowRef([]), - personNames = shallowRef>(), + personNames = + shallowRef>(), issueDetails = ref<{ [issuecode: string]: InducksIssueDetails }>({}), isLoadingCountryNames = ref(false), issuecodeDetails = ref>({}), @@ -67,16 +71,19 @@ export const coa = defineStore("coa", () => { EventOutput >({}), issuecodesByPublicationcode = ref< - EventOutput + SuccessfulEventOutput >({}), issuesByPublicationcode = ref< - Record> + Record< + string, + SuccessfulEventOutput + > >({}), issueCountsByCountrycode = ref< EventOutput >({}), issueCountsByPublicationcode = ref< - EventOutput + SuccessfulEventOutput >({}), issueQuotations = ref< SuccessfulEventOutput< @@ -193,7 +200,7 @@ export const coa = defineStore("coa", () => { }, fetchIssuecodeDetails = async ( issuecodes: string[], - withFields: ExtraSelectField[] = [], + withFields: ("title" | "fullyindexed")[] = [], ) => { const newIssuecodes = issuecodes.filter( (issuecode) => diff --git a/apps/web/src/stores/collection.ts b/apps/web/src/stores/collection.ts index 3bca88535..0a7cf1496 100644 --- a/apps/web/src/stores/collection.ts +++ b/apps/web/src/stores/collection.ts @@ -83,7 +83,9 @@ export const collection = defineStore("collection", () => { watchedAuthors = shallowRef(), marketplaceContactMethods = ref(), suggestions = - shallowRef>(), + shallowRef< + SuccessfulEventOutput + >(), subscriptions = shallowRef(), popularIssuesInCollection = ref<{ [issuecode: string]: number; diff --git a/apps/web/src/stores/marketplace.ts b/apps/web/src/stores/marketplace.ts index bbe0ad204..5a7e1d9b3 100644 --- a/apps/web/src/stores/marketplace.ts +++ b/apps/web/src/stores/marketplace.ts @@ -1,4 +1,4 @@ -import type { EventOutput } from "socket-call-client"; +import type { EventOutput, SuccessfulEventOutput } from "socket-call-client"; import type { ClientEvents as CollectionServices } from "~dm-services/collection"; import type { requestedIssue } from "~prisma-schemas/schemas/dm"; @@ -16,7 +16,10 @@ export const marketplace = defineStore("marketplace", () => { isLoadingIssueRequestsAsSeller = ref(false), isLoadingIssuesOnSaleByOthers = ref(false), contactMethods = ref<{ - [userId: number]: EventOutput; + [userId: number]: SuccessfulEventOutput< + CollectionServices, + "getContactMethods" + >; }>({}), sentRequestIssueIds = computed(() => issueRequestsAsBuyer.value?.map(({ issueId }) => issueId), diff --git a/apps/web/src/stores/public-collection.ts b/apps/web/src/stores/public-collection.ts index 3863e04fd..7b75ae2cd 100644 --- a/apps/web/src/stores/public-collection.ts +++ b/apps/web/src/stores/public-collection.ts @@ -1,6 +1,6 @@ import useCollection from "../composables/useCollection"; import { socketInjectionKey } from "../composables/useDmSocket"; -import { EventOutput } from "socket-call-client"; +import { SuccessfulEventOutput } from "socket-call-client"; import type { ClientEvents as PublicCollectionServices } from "~dm-services/public-collection"; export const publicCollection = defineStore("publicCollection", () => { @@ -9,7 +9,10 @@ export const publicCollection = defineStore("publicCollection", () => { const issues = shallowRef< - EventOutput["issues"] + SuccessfulEventOutput< + PublicCollectionServices, + "getPublicCollection" + >["issues"] >(), publicUsername = ref(), publicationUrlRoot = computed( diff --git a/apps/whattheduck/package.json b/apps/whattheduck/package.json index c1da522e5..6bba59ca6 100644 --- a/apps/whattheduck/package.json +++ b/apps/whattheduck/package.json @@ -60,7 +60,7 @@ "js-cookie": "^3.0.8", "pinia": "^4.0.3", "sass": "^1.105.0", - "socket-call-client": "^0.8.0", + "socket-call-client": "^0.10.0", "universal-cookie": "^7.2.2", "vue": "^3.5.43", "vue-chartjs": "^5.3.4", diff --git a/apps/whattheduck/pnpm-lock.yaml b/apps/whattheduck/pnpm-lock.yaml index 981269db4..af44b0d37 100644 --- a/apps/whattheduck/pnpm-lock.yaml +++ b/apps/whattheduck/pnpm-lock.yaml @@ -113,8 +113,8 @@ importers: specifier: ^1.105.0 version: 1.105.0 socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)) universal-cookie: specifier: ^7.2.2 version: 7.2.2 @@ -5640,8 +5640,8 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - socket-call-client@0.8.0: - resolution: {integrity: sha512-Jjk1KjvofAcDAzlrtu0oIW85u2xadfDeMGEqJQjz+8JA7SsCNKEzh2bw0x4YMLCK+b25nqEeoIcQjGujU77VWA==} + socket-call-client@0.10.0: + resolution: {integrity: sha512-2VdFULMF991RDjVpIRpj78LHrtzH4LzWe5yJyBFbpQ7OB0xarFc1zr81TcMwqc7+fPusLw1npEjYPPiTBG5VGg==} engines: {node: '>=22.0.0'} peerDependencies: axios-cache-interceptor: ^1.12.0 @@ -6802,7 +6802,7 @@ snapshots: '@ionic/utils-subprocess': 2.1.14 '@ionic/utils-terminal': 2.3.5 commander: 9.5.0 - debug: 4.3.4 + debug: 4.4.3 env-paths: 2.2.1 kleur: 4.1.5 native-run: 2.0.3 @@ -7557,7 +7557,7 @@ snapshots: '@ionic/utils-array@2.1.6': dependencies: - debug: 4.3.4 + debug: 4.4.3 tslib: 2.6.2 transitivePeerDependencies: - supports-color @@ -7565,7 +7565,7 @@ snapshots: '@ionic/utils-fs@3.1.7': dependencies: '@types/fs-extra': 8.1.5 - debug: 4.3.4 + debug: 4.4.3 fs-extra: 9.1.0 tslib: 2.6.2 transitivePeerDependencies: @@ -7589,10 +7589,10 @@ snapshots: dependencies: '@ionic/utils-object': 2.1.6 '@ionic/utils-terminal': 2.3.4 - debug: 4.3.4 + debug: 4.4.3 signal-exit: 3.0.7 tree-kill: 1.2.2 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -7609,8 +7609,8 @@ snapshots: '@ionic/utils-stream@3.1.6': dependencies: - debug: 4.3.4 - tslib: 2.6.2 + debug: 4.4.3 + tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -7629,8 +7629,8 @@ snapshots: '@ionic/utils-stream': 3.1.6 '@ionic/utils-terminal': 2.3.4 cross-spawn: 7.0.6 - debug: 4.3.4 - tslib: 2.6.2 + debug: 4.4.3 + tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -7650,12 +7650,12 @@ snapshots: '@ionic/utils-terminal@2.3.4': dependencies: '@types/slice-ansi': 4.0.0 - debug: 4.3.4 + debug: 4.4.3 signal-exit: 3.0.7 slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - tslib: 2.6.2 + tslib: 2.8.1 untildify: 4.0.0 wrap-ansi: 7.0.0 transitivePeerDependencies: @@ -12492,7 +12492,7 @@ snapshots: smart-buffer@4.2.0: {} - socket-call-client@0.8.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.43(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.43(typescript@6.0.3) diff --git a/packages/api/package.json b/packages/api/package.json index 307748c03..c72658bb4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -38,8 +38,9 @@ "nodemailer": "^8.0.11", "onnxruntime-node": "1.22.0-rev", "sharp-0-34": "npm:sharp@0.34.5", - "socket-call-server": "^0.9.0", + "socket-call-server": "^0.12.0", "socket.io": "^4.8.3", + "valibot": "^1.5.0", "~dm-types": "workspace:*", "~prisma-schemas": "workspace:*" }, diff --git a/packages/api/pnpm-lock.yaml b/packages/api/pnpm-lock.yaml index 0dac76728..c943e0469 100644 --- a/packages/api/pnpm-lock.yaml +++ b/packages/api/pnpm-lock.yaml @@ -66,11 +66,14 @@ importers: specifier: npm:sharp@0.34.5 version: sharp@0.34.5 socket-call-server: - specifier: ^0.9.0 - version: 0.9.0 + specifier: ^0.12.0 + version: 0.12.0(valibot@1.5.0(typescript@6.0.3)) socket.io: specifier: ^4.8.3 version: 4.8.3 + valibot: + specifier: ^1.5.0 + version: 1.5.0(typescript@6.0.3) ~dm-types: specifier: workspace:* version: link:../types @@ -1783,9 +1786,14 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - socket-call-server@0.9.0: - resolution: {integrity: sha512-yh3KIhXlp0jaE5x/XYs+vG5sZjZbsyzTBL9jyJqa2SXJoVhnqjiqk0l6LVtxagpjeMvhTa/UgQYx1L36TShavg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} + peerDependencies: + valibot: ^1.0.0 + peerDependenciesMeta: + valibot: + optional: true socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} @@ -1916,6 +1924,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3681,9 +3697,11 @@ snapshots: dependencies: semver: 7.8.5 - socket-call-server@0.9.0: + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -3826,6 +3844,10 @@ snapshots: util-deprecate@1.0.2: {} + valibot@1.5.0(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vary@1.1.2: {} wait-on@9.1.0: diff --git a/packages/api/services/app/index.ts b/packages/api/services/app/index.ts index b8c1f1ae6..38001c2e7 100644 --- a/packages/api/services/app/index.ts +++ b/packages/api/services/app/index.ts @@ -1,5 +1,7 @@ import { existsSync, readFileSync } from "fs"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import namespaces from "../namespaces"; @@ -31,7 +33,13 @@ export const getUpdateFileUrl = async (appInfos?: AppInfos) => { }; const listenEvents = () => ({ - getBundleUrl: (appInfos: AppInfos) => getUpdateFileUrl(appInfos), + getBundleUrl: ev( + v.optional( + v.object({ + version: v.string(), + }), + ), + )((appInfos) => getUpdateFileUrl(appInfos)), }); export const { client, server } = useSocketEvents( diff --git a/packages/api/services/auth/index.ts b/packages/api/services/auth/index.ts index 4f0f67875..600bd5599 100644 --- a/packages/api/services/auth/index.ts +++ b/packages/api/services/auth/index.ts @@ -2,6 +2,8 @@ import crypto from "crypto"; import jwt from "jsonwebtoken"; import type { Errorable } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient } from "~prisma-schemas/schemas/dm/client"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -16,153 +18,158 @@ import { validate, } from "../collection/user/util"; import namespaces from "../namespaces"; -import { - generateAccessToken, - getHashedPassword, - isValidEmail, - loginAs, -} from "./util"; +import { generateAccessToken, getHashedPassword, loginAs } from "./util"; const listenEvents = () => ({ - forgot: async (token: string) => - new Promise((resolve) => { - jwt.verify(token, process.env.TOKEN_SECRET as string, (err) => { - resolve({ error: err!.message || "" }); + forgot: ev(v.string())( + async (token) => + new Promise((resolve) => { + jwt.verify(token, process.env.TOKEN_SECRET as string, (err) => { + resolve({ error: err!.message || "" }); + }); + }), + ), + + requestTokenForForgotPassword: ev( + v.pipe(v.string(), v.email("Invalid email")), + )(async (email) => { + const user = await prismaClient.user.findFirst({ + where: { email }, + }); + if (user) { + console.log( + `A visitor requested to reset a password for a valid e-mail: ${email}`, + ); + const token = jwt.sign( + { exp: Math.floor(Date.now() / 1000) + 60 * 60, data: email }, + process.env.TOKEN_SECRET!, + ); + await prismaClient.userPasswordToken.create({ + data: { userId: user.id, token }, }); - }), - requestTokenForForgotPassword: async (email: string) => { - if (!isValidEmail(email)) { - return { error: "Invalid email" }; + + await new resetPassword({ user, token }).send(); + return { token }; } else { - const user = await prismaClient.user.findFirst({ - where: { email }, - }); - if (user) { - console.log( - `A visitor requested to reset a password for a valid e-mail: ${email}`, - ); - const token = jwt.sign( - { exp: Math.floor(Date.now() / 1000) + 60 * 60, data: email }, - process.env.TOKEN_SECRET!, + console.log( + `A visitor requested to reset a password for an invalid e-mail: ${email}`, + ); + return { error: "Invalid email" as const }; + } + }), + + changePassword: ev( + v.pipe( + v.object({ + password: v.string(), + password2: v.string(), + token: v.string(), + }), + v.check( + ({ password }) => password.length >= 6, + "Your password should be at least 6 characters long" as const, + ), + v.check( + ({ password, password2 }) => password === password2, + "The two passwords should be identical" as const, + ), + ), + )( + async ({ password, token }) => + new Promise>((resolve) => { + jwt.verify( + token, + process.env.TOKEN_SECRET as string, + async (err: unknown, data: unknown) => { + if (err) { + resolve({ error: "Invalid token" } as const); + } else { + const hashedPassword = crypto + .createHash("sha1") + .update(password) + .digest("hex"); + const email = (data as { data: string }).data; + await prismaClient.user.updateMany({ + data: { + password: hashedPassword, + }, + where: { + email, + }, + }); + const user = (await prismaClient.user.findFirst({ + where: { + email, + }, + }))!; + + resolve({ token: await loginAs(user, hashedPassword) } as const); + } + }, ); - await prismaClient.userPasswordToken.create({ - data: { userId: user.id, token }, - }); + }), + ), - await new resetPassword({ user, token }).send(); - return { token }; - } else { - console.log( - `A visitor requested to reset a password for an invalid e-mail: ${email}`, - ); - return { error: "Invalid email" }; - } - } - }, + getCsrf: async () => "", - changePassword: async ({ - password, - password2, - token, - }: { - password: string; - password2: string; - token: string; - }) => - new Promise>((resolve) => { - jwt.verify( - token, - process.env.TOKEN_SECRET as string, - async (err: unknown, data: unknown) => { - if (err) { - resolve({ error: "Invalid token" } as const); - } else if (password.length < 6) { - resolve({ - error: "Your password should be at least 6 characters long", - } as const); - } else if (password !== password2) { - resolve({ - error: "The two passwords should be identical", - } as const); + signup: ev( + v.object({ + username: v.string(), + password: v.string(), + email: v.string(), + }), + )( + async (input) => + new Promise>((resolve) => { + console.log(`signup with user ${input.username}`); + prismaDm.$transaction(async (transaction) => { + const scopedError = await validate(transaction, input, [ + new UsernameValidation(), + new UsernameCreationValidation(), + new EmailValidation(), + new EmailCreationValidation(), + new PasswordValidation(), + ]); + if (scopedError) { + resolve({ error: "Bad request", ...scopedError } as const); } else { - const hashedPassword = crypto - .createHash("sha1") - .update(password) - .digest("hex"); - const email = (data as { data: string }).data; - await prismaClient.user.updateMany({ + const { username, password, email } = input; + const hashedPassword = getHashedPassword(password); + const user = await transaction.user.create({ data: { + username, password: hashedPassword, - }, - where: { email, + signupDate: new Date(), }, }); - const user = (await prismaClient.user.findFirst({ - where: { - email, - }, - }))!; - resolve({ token: await loginAs(user, hashedPassword) } as const); - } - }, - ); - }), + const privileges = ( + await transaction.userPermission.findMany({ + where: { + username, + }, + }) + ).groupBy("role", "privilege"); + const token = generateAccessToken({ + id: user.id, + username, + hashedPassword, + privileges, + }); - getCsrf: async () => "", + resolve(token); + } + }); + }), + ), - signup: (input: { username: string; password: string; email: string }) => - new Promise>((resolve) => { - console.log(`signup with user ${input.username}`); - prismaDm.$transaction(async (transaction) => { - const scopedError = await validate(transaction, input, [ - new UsernameValidation(), - new UsernameCreationValidation(), - new EmailValidation(), - new EmailCreationValidation(), - new PasswordValidation(), - ]); - if (scopedError) { - resolve({ error: "Bad request", ...scopedError } as const); - } else { - const { username, password, email } = input; - const hashedPassword = getHashedPassword(password); - const user = await transaction.user.create({ - data: { - username, - password: hashedPassword, - email, - signupDate: new Date(), - }, - }); - - const privileges = ( - await transaction.userPermission.findMany({ - where: { - username, - }, - }) - ).groupBy("role", "privilege"); - const token = generateAccessToken({ - id: user.id, - username, - hashedPassword, - privileges, - }); - - resolve(token); - } - }); + login: ev( + v.object({ + username: v.string(), + password: v.string(), }), - - login: async ({ - username, - password, - }: { - username: string; - password: string; - }) => { + )(async ({ username, password }) => { const hashedPassword = getHashedPassword(password); const user = await prismaDm.user.findFirst({ where: { @@ -177,7 +184,7 @@ const listenEvents = () => ({ } else { return { error: "Invalid username or password" }; } - }, + }), loginAsDemo: async () => { const demoUser = await prismaDm.user.findFirst({ diff --git a/packages/api/services/auth/util.ts b/packages/api/services/auth/util.ts index f8ebc2a1e..f72feacca 100644 --- a/packages/api/services/auth/util.ts +++ b/packages/api/services/auth/util.ts @@ -13,11 +13,6 @@ type SocketWithUser = Socket< { user?: SessionUser } >; -const EMAIL_REGEX = - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,24}))$/; - -export const isValidEmail = (email: string) => EMAIL_REGEX.test(email); - export const generateAccessToken = (payload: Omit) => jwt.sign( { exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 14, data: payload }, diff --git a/packages/api/services/bookcase/index.ts b/packages/api/services/bookcase/index.ts index b4b99bc90..e25e9d532 100644 --- a/packages/api/services/bookcase/index.ts +++ b/packages/api/services/bookcase/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import type { BookcaseEdge } from "~dm-types/BookcaseEdge"; import type { SessionUser } from "~dm-types/SessionUser"; @@ -41,7 +43,7 @@ const getLastPublicationPosition = async (userId: number) => .then((results) => results._max.order || -1); const listenEvents = ({ _socket }: UserServices) => ({ - getBookcaseOrder: async (username: string) => { + getBookcaseOrder: ev(v.string())(async (username) => { const user = await checkValidBookcaseUser(_socket.data.user, username); if ("error" in user) { return { error: user.error }; @@ -131,8 +133,8 @@ const listenEvents = ({ _socket }: UserServices) => ({ ).map(({ publicationcode }) => publicationcode), }; } - }, - getBookcase: async (username: string) => { + }), + getBookcase: ev(v.string())(async (username) => { const user = await checkValidBookcaseUser(null, username); if ("error" in user) { return { error: user.error }; @@ -178,9 +180,9 @@ const listenEvents = ({ _socket }: UserServices) => ({ })), ) .then((edges) => ({ edges })); - }, + }), - getBookcaseOptions: async (username: string) => { + getBookcaseOptions: ev(v.string())(async (username) => { const user = await checkValidBookcaseUser(null, username); return "error" in user ? { error: user.error } @@ -191,7 +193,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ }, showAllCopies: user.showDuplicatesInBookcase, }; - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/packages/api/services/bookcase/private/index.ts b/packages/api/services/bookcase/private/index.ts index d3aa26e45..e5a0ef613 100644 --- a/packages/api/services/bookcase/private/index.ts +++ b/packages/api/services/bookcase/private/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -7,13 +9,15 @@ import { RequiredAuthMiddleware } from "../../auth/util"; import namespaces from "../../namespaces"; const listenEvents = ({ _socket }: UserServices) => ({ - setBookcaseOptions: async ({ - textures, - showAllCopies: showDuplicatesInBookcase, - }: { - textures: { bookcase: string; bookshelf: string }; - showAllCopies: boolean; - }) => { + setBookcaseOptions: ev( + v.object({ + textures: v.object({ + bookcase: v.string(), + bookshelf: v.string(), + }), + showAllCopies: v.boolean(), + }), + )(async ({ textures, showAllCopies: showDuplicatesInBookcase }) => { const [, bookcaseSubTexture1] = textures.bookcase.split("/"); const [, bookcaseSubTexture2] = textures.bookshelf.split("/"); const user = await prismaDm.user.findUnique({ @@ -36,8 +40,9 @@ const listenEvents = ({ _socket }: UserServices) => ({ where: { id: user.id }, }); return "OK"; - }, - setBookcaseOrder: async (publicationCodes: string[]) => { + }), + + setBookcaseOrder: ev(v.array(v.string()))(async (publicationCodes) => { const userId = _socket.data.user.id; await prismaDm.bookcasePublicationOrder.deleteMany({ where: { userId }, @@ -49,7 +54,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ userId, })), }); - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/packages/api/services/bookstores/index.ts b/packages/api/services/bookstores/index.ts index 6d855812c..6a6039e3b 100644 --- a/packages/api/services/bookstores/index.ts +++ b/packages/api/services/bookstores/index.ts @@ -1,10 +1,7 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; -import type { - NewBookstore, - NewComment, - SimpleBookstore, -} from "~dm-types/SimpleBookstore"; import type { bookstore, bookstoreComment, @@ -82,7 +79,7 @@ const getBookstores = ( const adminListenEvents = () => ({ getBookstores: () => getBookstores(), - approveBookstoreComment: async (commentId: number) => { + approveBookstoreComment: ev(v.number())(async (commentId) => { let bookstoreComment: bookstoreComment; try { bookstoreComment = await prismaDm.bookstoreComment.findUniqueOrThrow({ @@ -113,7 +110,7 @@ const adminListenEvents = () => ({ }); await persistContribution(user, 1, bookstoreComment); } - }, + }), }); export const { client: adminClient, server: adminServer } = useSocketEvents< @@ -127,7 +124,7 @@ export const { client: adminClient, server: adminServer } = useSocketEvents< const listenEvents = ({ _socket }: UserServices) => ({ getActiveBookstores: () => getBookstores(true), - reportBookstoreAsClosed: async (bookstoreId: number) => { + reportBookstoreAsClosed: ev(v.number())(async (bookstoreId) => { const bookstore = await prismaDm.bookstore.findUniqueOrThrow({ where: { id: bookstoreId }, }); @@ -136,15 +133,27 @@ const listenEvents = ({ _socket }: UserServices) => ({ bookstoreId, bookstoreName: bookstore.name, }).send(); - }, - - createBookstoreComment: async ( - bookstore: NewBookstore | SimpleBookstore, - comment: NewComment, - ) => { - if (!bookstore.name || ("id" in bookstore && !bookstore.id)) { - return { error: "No bookstore ID or name was provided" }; - } + }), + + createBookstoreComment: ev( + v.union([ + v.object({ + id: v.number(), + }), + v.object({ + name: v.pipe(v.string(), v.nonEmpty()), + address: v.string(), + coordX: v.number(), + coordY: v.number(), + }), + ]), + v.object({ + comment: v.string(), + atmosphereRating: v.number(), + pricesRating: v.number(), + selectionRating: v.number(), + }), + )(async (bookstore, comment) => { const user = _socket.data.user ? await prismaDm.user.findUnique({ where: { @@ -201,7 +210,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ }).send(); return createdComment; - }, + }), }); export type AdminClientEvents = (typeof adminClient)["emitEvents"]; diff --git a/packages/api/services/coa/authors/index.ts b/packages/api/services/coa/authors/index.ts index 8fb7a3d40..3a5e7bd75 100644 --- a/packages/api/services/coa/authors/index.ts +++ b/packages/api/services/coa/authors/index.ts @@ -1,10 +1,13 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; export default { - getAuthorList: async (personcodes: string[]) => + getAuthorList: ev(v.array(v.string()))(async (personcodes) => getAuthorFullNames([...new Set(personcodes)]), + ), - searchAuthor: async (partialAuthorName: string) => + searchAuthor: ev(v.string())(async (partialAuthorName) => prismaCoa.inducks_person .findMany({ where: { @@ -22,6 +25,7 @@ export default { })) .groupBy("personcode", "fullname"), ), + ), }; export const getAuthorFullNames = (authorPersoncodes: string[]) => diff --git a/packages/api/services/coa/countries/index.ts b/packages/api/services/coa/countries/index.ts index 1c51bc8f2..4c653915c 100644 --- a/packages/api/services/coa/countries/index.ts +++ b/packages/api/services/coa/countries/index.ts @@ -1,9 +1,14 @@ import { Prisma } from "~prisma-schemas/schemas/coa"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default { - getCountryList: async (locale: string, countryCodes?: string[]) => - getCountryNames(locale, countryCodes), + getCountryList: ev( + v.string(), + v.optional(v.array(v.string())), + )(async (locale, countryCodes) => getCountryNames(locale, countryCodes)), }; const getCountryNames = async ( diff --git a/packages/api/services/coa/issue-details/index.ts b/packages/api/services/coa/issue-details/index.ts index 65056da48..89d9a2974 100644 --- a/packages/api/services/coa/issue-details/index.ts +++ b/packages/api/services/coa/issue-details/index.ts @@ -1,6 +1,8 @@ import type { SimpleEntry } from "~dm-types/SimpleEntry"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; export const getPrefixedEntryurl = (url: string, sitecode: string) => `${ @@ -33,10 +35,9 @@ export const getPopularityByIssuecodes = async (issuecodes: string[]) => .then((data) => data.groupBy("issuecode")); export default { - getIssueDetails: async (issuecode: string) => { - if (typeof issuecode !== "string" || !issuecode) { - return { error: "Invalid issuecode" }; - } + getIssueDetails: ev( + v.pipe(v.string("Invalid issuecode"), v.nonEmpty("Invalid issuecode")), + )(async (issuecode) => { const issue = await prismaCoa.inducks_issue.findFirst({ where: { issuecode }, }); @@ -47,25 +48,27 @@ export default { releaseDate: issue.oldestdate!, entries: await getEntries(issuecode), }; - }, + }), - getIssueCoverDetails: async (issuecodes: string[]) => - issuecodes.length > 10 - ? { error: "Too many requests" } - : getIssueCoverDetails(issuecodes), + getIssueCoverDetails: ev( + v.pipe(v.array(v.string()), v.maxLength(10, "Too many requests")), + )(async (issuecodes) => getIssueCoverDetails(issuecodes)), - getIssueCoverDetailsByPublicationcode: async (publicationcode: string) => { - const issuecodes = ( - await prismaCoa.inducks_issue.findMany({ - select: { issuecode: true }, - where: { publicationcode }, - }) - ).map(({ issuecode }) => issuecode); - return getIssueCoverDetails(issuecodes); - }, + getIssueCoverDetailsByPublicationcode: ev(v.string())( + async (publicationcode) => { + const issuecodes = ( + await prismaCoa.inducks_issue.findMany({ + select: { issuecode: true }, + where: { publicationcode }, + }) + ).map(({ issuecode }) => issuecode); + return getIssueCoverDetails(issuecodes); + }, + ), - getIssuePopularities: (issuecodes: string[]) => + getIssuePopularities: ev(v.array(v.string()))(async (issuecodes) => getPopularityByIssuecodes(issuecodes), + ), }; export const getCoverUrls = async (issuecodes: string[]) => { diff --git a/packages/api/services/coa/issues/index.ts b/packages/api/services/coa/issues/index.ts index b4fab4c32..4a8c6c5a2 100644 --- a/packages/api/services/coa/issues/index.ts +++ b/packages/api/services/coa/issues/index.ts @@ -1,9 +1,14 @@ import type { IssueWithIssuecodeOnly } from "~dm-types/IssueWithIssuecodeOnly"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; -import type { ExtraSelectField } from "~prisma-schemas/schemas/coa/extended"; + +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; export default { - getIssues: (issuecodes: string[], withFields: ExtraSelectField[]) => + getIssues: ev( + v.array(v.string()), + v.array(v.union([v.literal("title"), v.literal("fullyindexed")])), + )((issuecodes, withFields) => prismaCoa .augmentIssueArrayWithInducksData( issuecodes.map((issuecode) => ({ issuecode })), @@ -12,8 +17,9 @@ export default { ), ) .then((data) => data.groupBy("issuecode")), + ), - getCoaCountByPublicationcode: (publicationcodes: string[]) => + getCoaCountByPublicationcode: ev(v.array(v.string()))((publicationcodes) => prismaCoa.inducks_issue .groupBy({ _count: { @@ -34,8 +40,9 @@ export default { ]), ), ), + ), - getCoaCountByCountrycode: (countrycodes: string[]) => + getCoaCountByCountrycode: ev(v.array(v.string()))((countrycodes) => prismaCoa.inducks_issue .groupBy({ _count: { @@ -60,27 +67,30 @@ export default { {}, ), ), + ), - getIssuecodesByPublicationcodes: async (publicationcodes: string[]) => - prismaCoa.inducks_issue - .findMany({ - select: { - publicationcode: true, - issuecode: true, - issuenumber: true, - }, - where: { - publicationcode: { - in: publicationcodes.filter((p): p is string => !!p), + getIssuecodesByPublicationcodes: ev(v.array(v.string()))( + async (publicationcodes) => + prismaCoa.inducks_issue + .findMany({ + select: { + publicationcode: true, + issuecode: true, + issuenumber: true, }, - }, - orderBy: { - issuecode: "asc", - }, - }) - .then((data) => data.groupBy("publicationcode", "issuecode[]")), + where: { + publicationcode: { + in: publicationcodes.filter((p): p is string => !!p), + }, + }, + orderBy: { + issuecode: "asc", + }, + }) + .then((data) => data.groupBy("publicationcode", "issuecode[]")), + ), - getIssuesByPublicationcode: async (publicationcode: string) => + getIssuesByPublicationcode: ev(v.string())(async (publicationcode) => prismaCoa.inducks_issue.findMany({ select: { issuecode: true, @@ -93,9 +103,11 @@ export default { issuecode: "asc", }, }), + ), - getIssuesByStorycode: async (storycode: string) => - prismaCoa.$queryRaw` + getIssuesByStorycode: ev(v.string())( + async (storycode) => + prismaCoa.$queryRaw` SELECT publicationcode, issuenumber, issuecode FROM inducks_issue issue INNER JOIN inducks_entry entry using (issuecode) @@ -103,6 +115,7 @@ export default { WHERE sv.storycode = ${storycode} GROUP BY publicationcode, issuenumber ORDER BY publicationcode`, + ), getRecentIssues: () => prismaCoa.inducks_issue.findMany({ diff --git a/packages/api/services/coa/publications/index.ts b/packages/api/services/coa/publications/index.ts index e2ecb9dd8..a5a820ea8 100644 --- a/packages/api/services/coa/publications/index.ts +++ b/packages/api/services/coa/publications/index.ts @@ -1,30 +1,37 @@ import type { Prisma as PrismaCoa } from "~prisma-schemas/schemas/coa"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default { - getPublicationLanguagecode: (publicationcode: string) => + getPublicationLanguagecode: ev(v.string())((publicationcode) => prismaCoa.inducks_publication .findUnique({ where: { publicationcode }, select: { languagecode: true }, }) .then((publication) => publication?.languagecode ?? null), + ), - getPublicationListFromCountrycodes: (countrycodes: string[]) => + getPublicationListFromCountrycodes: ev(v.array(v.string()))((countrycodes) => getPublicationTitles({ OR: countrycodes.map((countrycode) => ({ publicationcode: { startsWith: `${countrycode}/` }, })), }), + ), getFullPublicationList: () => getPublicationTitles(), - getPublicationListFromPublicationcodeList: (publicationcodes: string[]) => - getPublicationTitles( - publicationcodes.length - ? { publicationcode: { in: publicationcodes } } - : {}, - ), + getPublicationListFromPublicationcodeList: ev(v.array(v.string()))( + (publicationcodes) => + getPublicationTitles( + publicationcodes.length + ? { publicationcode: { in: publicationcodes } } + : {}, + ), + ), }; export const getPublicationTitles = async ( diff --git a/packages/api/services/coa/quotations/index.ts b/packages/api/services/coa/quotations/index.ts index 471872d90..3b9051c2a 100644 --- a/packages/api/services/coa/quotations/index.ts +++ b/packages/api/services/coa/quotations/index.ts @@ -1,7 +1,10 @@ import type { inducks_issuequotation } from "~prisma-schemas/schemas/coa"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; -const ISSUE_CODE_REGEX = /[a-z]+\/[-A-Z0-9 ]+/g; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + +const ISSUE_CODE_REGEX = /[a-z]+\/[-A-Z0-9 ]+/; export const getShownQuotations = < Quotation extends Pick< @@ -34,18 +37,16 @@ export const getQuotationsByIssuecodes = async (issuecodes: string[]) => .then(getShownQuotations); export default { - getQuotationsByIssuecodes: async (issuesByIssuecodes: string[]) => { - const codes = issuesByIssuecodes.filter((code) => - ISSUE_CODE_REGEX.test(code), - ); - if (!codes.length) { - return Promise.resolve({ error: "Bad request" }); - } else if (codes.length > 4) { - return Promise.resolve({ error: "Too many requests" }); - } else { - return { - quotations: await getQuotationsByIssuecodes(codes), - }; - } - }, + getQuotationsByIssuecodes: ev( + v.pipe( + v.array(v.string()), + v.transform((issuecodes) => + issuecodes.filter((code) => ISSUE_CODE_REGEX.test(code)), + ), + v.nonEmpty("Bad request" as const), + v.maxLength(4, "Too many requests" as const), + ), + )(async (codes) => ({ + quotations: await getQuotationsByIssuecodes(codes), + })), }; diff --git a/packages/api/services/collection/index.ts b/packages/api/services/collection/index.ts index 21f5ba760..406b42062 100644 --- a/packages/api/services/collection/index.ts +++ b/packages/api/services/collection/index.ts @@ -1,6 +1,9 @@ import PushNotifications from "@pusher/push-notifications-server"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../index"; @@ -52,7 +55,7 @@ const listenEvents = (services: UserServices) => { results.groupBy("issuecode", "popularity"), ), - getNotificationToken: async (username: string) => { + getNotificationToken: ev(v.string())(async (username: string) => { if (username !== _socket.data.user.username) { return { error: "Unauthorized" }; } else { @@ -66,35 +69,34 @@ const listenEvents = (services: UserServices) => { return { error: "Error", errorDetails: (e as Error).message }; } } - }, + }), - getLastVisit: async () => { - let user: Awaited>; - try { - user = await getUser(_socket.data.user.id); - } catch (_e) { - return { error: "This user does not exist" }; - } - if (!user.lastAccess) { - console.log( - `Initializing last access for user ${_socket.data.user.id}`, - ); - user.previousAccess = null; - user.lastAccess = new Date(); - } else { - console.log(`Updating last access for user ${_socket.data.user.id}`); - user.previousAccess = user.lastAccess; - user.lastAccess = new Date(); - } - prismaDm.user.update({ - data: user, - where: { - id: _socket.data.user.id, - }, - }); + getLastVisit: async () => + getUser(_socket.data.user.id) + .then((user) => { + if (!user.lastAccess) { + console.log( + `Initializing last access for user ${_socket.data.user.id}`, + ); + user.previousAccess = null; + user.lastAccess = new Date(); + } else { + console.log( + `Updating last access for user ${_socket.data.user.id}`, + ); + user.previousAccess = user.lastAccess; + user.lastAccess = new Date(); + } + prismaDm.user.update({ + data: user, + where: { + id: _socket.data.user.id, + }, + }); - return user.previousAccess?.toISOString() || null; - }, + return user.previousAccess?.toISOString() || null; + }) + .catch(() => ({ error: "This user does not exist" as const })), getLastPublishedEdges: async () => { const threeMonthsAgo = new Date(); diff --git a/packages/api/services/collection/issues/index.ts b/packages/api/services/collection/issues/index.ts index 1a3194276..b68afec41 100644 --- a/packages/api/services/collection/issues/index.ts +++ b/packages/api/services/collection/issues/index.ts @@ -2,10 +2,9 @@ import { parse } from "csv-parse/sync"; import { existsSync, readFileSync } from "fs"; import { cwd } from "process"; -import type { - CollectionUpdateMultipleIssues, - CollectionUpdateSingleIssue, -} from "~dm-types/CollectionUpdate"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + import type { InducksIssueQuotationSimple } from "~dm-types/InducksIssueQuotationSimple"; import type { TransactionResults } from "~dm-types/TransactionResults"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; @@ -17,6 +16,14 @@ import type { UserServices } from "../../../index"; import { getShownQuotations } from "../../coa/quotations"; import { checkPurchaseIdsBelongToUser, deleteIssues } from "./util"; +const purchaseIdSchema = v.union([ + v.nullable(v.number()), + v.pipe( + v.literal(""), + v.transform(() => null), + ), +]); + export default ({ _socket }: UserServices) => ({ getIssues: async () => { if (_socket.data.user.username === "demo") { @@ -46,7 +53,10 @@ export default ({ _socket }: UserServices) => ({ ); }, - setIssuesAside: async (issueIds: number[], buyerId: number) => { + setIssuesAside: ev( + v.array(v.number()), + v.number(), + )(async (issueIds, buyerId) => { await prismaDm.requestedIssue.createMany({ data: issueIds.map((issueId) => ({ issueId, @@ -54,8 +64,11 @@ export default ({ _socket }: UserServices) => ({ isBooked: true, })), }); - }, - transferIssues: async (issueIds: number[], buyerId: number) => { + }), + transferIssues: ev( + v.array(v.number()), + v.number(), + )(async (issueIds, buyerId) => { await prismaDm.issue.updateMany({ data: { userId: buyerId, @@ -75,14 +88,16 @@ export default ({ _socket }: UserServices) => ({ buyerId, }, }); - }, - - addOrChangeIssues: async ({ - issuecodes, - purchaseId, - condition, - labelIds, - }: CollectionUpdateMultipleIssues) => { + }), + + addOrChangeIssues: ev( + v.object({ + issuecodes: v.array(v.string()), + purchaseId: v.optional(purchaseIdSchema), + condition: v.optional(v.union([v.null(), v.enum(issue_condition)])), + labelIds: v.optional(v.array(v.number())), + }), + )(async ({ issuecodes, purchaseId, condition, labelIds }) => { const user = _socket.data.user; let checkedPurchaseId: number | null = null; @@ -103,17 +118,27 @@ export default ({ _socket }: UserServices) => ({ checkedPurchaseId, labelIds, ); - }, - addOrChangeCopies: async ({ - issuecode, - copies, - }: CollectionUpdateSingleIssue) => { + }), + + addOrChangeCopies: ev( + v.object({ + issuecode: v.string(), + copies: v.array( + v.object({ + id: v.nullable(v.number()), + condition: v.nullable(v.enum(issue_condition)), + purchaseId: purchaseIdSchema, + labelIds: v.array(v.number()), + }), + ), + }), + )(async ({ issuecode, copies }) => { const userId = _socket.data.user.id; const checkedPurchaseIds = await checkPurchaseIdsBelongToUser( copies .map(({ purchaseId }) => purchaseId) - .filter((purchaseId) => !!purchaseId) as number[], + .filter((purchaseId): purchaseId is number => !!purchaseId), userId, ); @@ -127,24 +152,22 @@ export default ({ _socket }: UserServices) => ({ ); return output; - }, + }), - getCollectionQuotations: (): Promise< - Record - > => + getCollectionQuotations: () => prismaDm.$queryRaw` - select - issuecode, - round(min(estimationmin)) AS estimationMin, - case max(ifnull(estimationmax, 0)) - when 0 then null - else round(max(ifnull(estimationmax, 0))) end AS estimationMax - from dm.numeros - inner join coa.inducks_issuequotation using (issuecode) - where ID_Utilisateur = ${_socket.data.user.id} - and estimationmin is not null - group by numeros.ID; - `.then(getShownQuotations), + select + issuecode, + round(min(estimationmin)) AS estimationMin, + case max(ifnull(estimationmax, 0)) + when 0 then null + else round(max(ifnull(estimationmax, 0))) end AS estimationMax + from dm.numeros + inner join coa.inducks_issuequotation using (issuecode) + where ID_Utilisateur = ${_socket.data.user.id} + and estimationmin is not null + group by numeros.ID; + `.then(getShownQuotations), }); const addOrChangeIssues = async ( @@ -153,7 +176,7 @@ const addOrChangeIssues = async ( condition: issue_condition | undefined, purchaseId: number | null | undefined, _labelIds: number[] | undefined, -): Promise => { +) => { const existingIssues = await prismaDm.issue.findMany({ where: { issuecode: { diff --git a/packages/api/services/collection/labels/index.ts b/packages/api/services/collection/labels/index.ts index 0b20143b4..2c81e70f8 100644 --- a/packages/api/services/collection/labels/index.ts +++ b/packages/api/services/collection/labels/index.ts @@ -2,6 +2,9 @@ import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../../index"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export const getUserLabel = async (description: string, userId: number) => prismaDm.label.findUnique({ where: { @@ -21,7 +24,7 @@ export default ({ _socket }: UserServices) => ({ }, }), - createLabel: async (description: string) => { + createLabel: ev(v.string())(async (description) => { const criteria = { userId: _socket.data.user.id, description, @@ -40,9 +43,9 @@ export default ({ _socket }: UserServices) => ({ await prismaDm.label.create({ data: criteria, }); - }, + }), - deleteLabel: async (labelDescription: string) => { + deleteLabel: ev(v.string())(async (labelDescription) => { const criteria = { userId: _socket.data.user.id, description: labelDescription, @@ -56,5 +59,5 @@ export default ({ _socket }: UserServices) => ({ description_userId: criteria, }, }); - }, + }), }); diff --git a/packages/api/services/collection/marketplace/contact-methods/index.ts b/packages/api/services/collection/marketplace/contact-methods/index.ts index 74963c282..8a55a32db 100644 --- a/packages/api/services/collection/marketplace/contact-methods/index.ts +++ b/packages/api/services/collection/marketplace/contact-methods/index.ts @@ -4,8 +4,11 @@ import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../../../index"; import { getIssuesForSale } from ".."; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default ({ _socket }: UserServices) => ({ - getContactMethods: async (sellerId: number) => { + getContactMethods: ev(v.number())(async (sellerId) => { const issuesForSale = await getIssuesForSale(_socket.data.user.id); if (!issuesForSale.some((issue) => issue.userId === sellerId)) { return { error: "Invalid seller ID", errorDetails: String(sellerId) }; @@ -28,5 +31,5 @@ export default ({ _socket }: UserServices) => ({ }), {}, ); - }, + }), }); diff --git a/packages/api/services/collection/marketplace/index.ts b/packages/api/services/collection/marketplace/index.ts index aed29548c..affaf8805 100644 --- a/packages/api/services/collection/marketplace/index.ts +++ b/packages/api/services/collection/marketplace/index.ts @@ -1,6 +1,8 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import type { issue } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import type { UserServices } from "../../../index"; import contactMethods from "./contact-methods"; @@ -11,19 +13,21 @@ export default (services: UserServices) => { return { ...contactMethods(services), - deleteRequests: async (issueId: number) => { + deleteRequests: ev(v.number())(async (issueId) => { await prismaDm.requestedIssue.deleteMany({ where: { buyerId: _socket.data.user.id, issueId, }, }); - }, + }), - createRequests: async (issueIds: number[]) => { - if (issueIds.find((issueId) => isNaN(issueId))) { - return { error: `Invalid issue ID list, NaN` }; - } + createRequests: ev( + v.pipe( + v.array(v.number(`Invalid issue ID list, NaN`)), + v.nonEmpty("Invalid issue ID list"), + ), + )(async (issueIds) => { const issues = await prismaDm.issue.findMany({ where: { id: { in: issueIds }, @@ -57,33 +61,35 @@ export default (services: UserServices) => { issueId, })), }); - }, + }), - getRequests: async (as: "buyer" | "seller") => { - switch (as) { - case "seller": { - const requestedIssuesOnSaleIds = await prismaDm.$queryRaw< - { id: number }[] - >` + getRequests: ev(v.union([v.literal("buyer"), v.literal("seller")]))( + async (as) => { + switch (as) { + case "seller": { + const requestedIssuesOnSaleIds = await prismaDm.$queryRaw< + { id: number }[] + >` SELECT requestedIssue.ID AS id FROM numeros_demandes requestedIssue INNER JOIN numeros issue ON requestedIssue.ID_Numero = issue.ID WHERE issue.ID_Utilisateur = ${_socket.data.user.id} `; - return await prismaDm.requestedIssue.findMany({ - where: { - id: { in: requestedIssuesOnSaleIds.map(({ id }) => id) }, - }, - }); + return await prismaDm.requestedIssue.findMany({ + where: { + id: { in: requestedIssuesOnSaleIds.map(({ id }) => id) }, + }, + }); + } + case "buyer": + return await prismaDm.requestedIssue.findMany({ + where: { + buyerId: _socket.data.user.id, + }, + }); } - case "buyer": - return await prismaDm.requestedIssue.findMany({ - where: { - buyerId: _socket.data.user.id, - }, - }); - } - }, + }, + ), getIssuesForSale: () => getIssuesForSale(_socket.data.user.id), }; diff --git a/packages/api/services/collection/options/index.ts b/packages/api/services/collection/options/index.ts index 283c08995..74f71d9c9 100644 --- a/packages/api/services/collection/options/index.ts +++ b/packages/api/services/collection/options/index.ts @@ -3,6 +3,15 @@ import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../../index"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + +const userOptionTypeValidation = v.union([ + v.literal("suggestion_notification_country"), + v.literal("sales_notification_publications"), + v.literal("marketplace_contact_methods"), +]); + const optionNameToEnum = ( optionName: | "suggestion_notification_country" @@ -11,7 +20,7 @@ const optionNameToEnum = ( ) => userOptionType[optionName]; export default ({ _socket }: UserServices) => ({ - getOption: async (optionName: Parameters[0]) => + getOption: ev(userOptionTypeValidation)(async (optionName) => prismaDm.userOption .findMany({ where: { @@ -20,8 +29,12 @@ export default ({ _socket }: UserServices) => ({ }, }) .then((data) => data.map(({ optionValue }) => optionValue)), + ), - setOption: async (optionName: userOptionType, optionValues: string[]) => { + setOption: ev( + userOptionTypeValidation, + v.array(v.string()), + )(async (optionName, optionValues) => { { const userId = _socket.data.user.id; await prismaDm.userOption.deleteMany({ @@ -43,5 +56,5 @@ export default ({ _socket }: UserServices) => ({ ), ); } - }, + }), }); diff --git a/packages/api/services/collection/purchases/index.ts b/packages/api/services/collection/purchases/index.ts index 4ece7c8e3..83b5b8db2 100644 --- a/packages/api/services/collection/purchases/index.ts +++ b/packages/api/services/collection/purchases/index.ts @@ -3,6 +3,9 @@ import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../../index"; import { getUserPurchase } from "../issues/util"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default ({ _socket }: UserServices) => ({ getPurchases: () => prismaDm.purchase @@ -21,15 +24,21 @@ export default ({ _socket }: UserServices) => ({ })), ), - createPurchase: async (date: string, description: string) => { + createPurchase: ev( + v.pipe( + v.string(), + v.check( + (date) => !Number.isNaN(new Date(date).getTime()), + "Invalid date" as const, + ), + ), + v.string(), + )(async (date, description) => { const criteria = { userId: _socket.data.user.id, date: new Date(date), description, }; - if (Number.isNaN(criteria.date.getTime())) { - return { error: `Invalid date: ${date}` } as const; - } if ( (await prismaDm.purchase.count({ @@ -42,9 +51,9 @@ export default ({ _socket }: UserServices) => ({ await prismaDm.purchase.create({ data: criteria, }); - }, + }), - deletePurchase: async (purchaseId: number) => { + deletePurchase: ev(v.number())(async (purchaseId) => { const criteria = { userId: _socket.data.user.id, id: purchaseId, @@ -56,5 +65,5 @@ export default ({ _socket }: UserServices) => ({ await prismaDm.purchase.deleteMany({ where: criteria, }); - }, + }), }); diff --git a/packages/api/services/collection/subscriptions/index.ts b/packages/api/services/collection/subscriptions/index.ts index dfd00dca5..c70e032ac 100644 --- a/packages/api/services/collection/subscriptions/index.ts +++ b/packages/api/services/collection/subscriptions/index.ts @@ -1,9 +1,11 @@ -import type { EditSubscription } from "~dm-types/EditSubscription"; import type { subscription } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../../index"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export type SubscriptionTransformedStringDates = Omit< subscription, "startDate" | "endDate" @@ -30,18 +32,28 @@ export default ({ _socket }: UserServices) => ({ })), ), - createSubscription: async (subscription: EditSubscription) => { + createSubscription: ev( + v.object({ + publicationcode: v.string(), + startDate: v.string(), + endDate: v.string(), + }), + )(async (subscription) => { await upsertSubscription(null, subscription, _socket.data.user.id); - }, + }), - updateSubscription: async ( - id: number, - subscription: SubscriptionTransformedStringDates, - ) => { + updateSubscription: ev( + v.number(), + v.object({ + publicationcode: v.string(), + startDate: v.string(), + endDate: v.string(), + }), + )(async (id, subscription) => { await upsertSubscription(id, subscription, _socket.data.user.id); - }, + }), - deleteSubscription: async (id: number) => { + deleteSubscription: ev(v.number())(async (id) => { await prismaDm.subscription.deleteMany({ where: { id, @@ -50,12 +62,16 @@ export default ({ _socket }: UserServices) => ({ }, }, }); - }, + }), }); export async function upsertSubscription( id: number | null, - subscription: EditSubscription, + subscription: { + publicationcode: string; + startDate: string; + endDate: string; + }, userId: number, ) { if (!subscription.publicationcode) { diff --git a/packages/api/services/collection/user/index.ts b/packages/api/services/collection/user/index.ts index f644b70fc..fe10ca14c 100644 --- a/packages/api/services/collection/user/index.ts +++ b/packages/api/services/collection/user/index.ts @@ -1,6 +1,5 @@ import type { Errorable } from "socket-call-server"; -import type { UserForAccountForm } from "~dm-types/UserForAccountForm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import PresentationSentenceRequested from "../../../emails/presentation-sentence-requested"; @@ -19,6 +18,9 @@ import { validate, } from "./util"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default ({ _socket }: UserServices) => ({ getUser: async () => getUser(_socket.data.user.id).catch(() => ({ error: "User not found" })), @@ -42,7 +44,24 @@ export default ({ _socket }: UserServices) => ({ }); }, - updateUser: async (input: UserForAccountForm) => { + updateUser: ev( + v.pipe( + v.object({ + discordId: v.optional(v.string()), + email: v.string(), + allowSharing: v.boolean(), + marketplaceAcceptsExchanges: v.boolean(), + presentationText: v.string(), + oldPassword: v.optional(v.string()), + password: v.optional(v.string()), + password2: v.optional(v.string()), + }), + v.check( + ({ password, password2 }) => password === password2, + "The two passwords should be identical" as const, + ), + ), + )(async (input) => { let hasRequestedPresentationSentenceUpdate = false; let validators: Validation[] = [ new DiscordIdValidation(), @@ -50,7 +69,6 @@ export default ({ _socket }: UserServices) => ({ new EmailUpdateValidation(), new PresentationTextValidation(), ]; - input.userId = _socket.data.user.id; if (input.password) { validators = [ ...validators, @@ -68,7 +86,15 @@ export default ({ _socket }: UserServices) => ({ let hasResolved = false; prismaDm .$transaction(async (transaction) => { - const scopedError = await validate(transaction, input, validators); + const scopedError = await validate( + transaction, + { + ...input, + id: _socket.data.user.id, + userId: _socket.data.user.id, + }, + validators, + ); if (scopedError) { resolve({ error: "Bad request", ...scopedError } as const); hasResolved = true; @@ -117,5 +143,5 @@ export default ({ _socket }: UserServices) => ({ } }); }); - }, + }), }); diff --git a/packages/api/services/collection/user/util.ts b/packages/api/services/collection/user/util.ts index cde3600dd..0eea0ed40 100644 --- a/packages/api/services/collection/user/util.ts +++ b/packages/api/services/collection/user/util.ts @@ -1,7 +1,7 @@ import type { user } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; - -import { getHashedPassword, isValidEmail } from "../../auth/util"; +import * as v from "valibot"; +import { getHashedPassword } from "../../auth/util"; type PrismaDmTransaction = Parameters< Parameters[0] @@ -126,7 +126,7 @@ export class EmailUpdateValidation extends Validation { export class EmailValidation extends Validation { run = async ({ email }: Pick) => { - if (!isValidEmail(email)) { + if (!v.is(v.pipe(v.string(), v.email()), email)) { return { message: "Adresse e-mail invalide", selector: "#email", diff --git a/packages/api/services/collection/watched-authors/index.ts b/packages/api/services/collection/watched-authors/index.ts index d47b3295b..058cf83c4 100644 --- a/packages/api/services/collection/watched-authors/index.ts +++ b/packages/api/services/collection/watched-authors/index.ts @@ -1,9 +1,11 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; -import type { authorUser } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { UserServices } from "../../../index"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + const maxWatchedAuthors = 5; export default ({ _socket }: UserServices) => ({ @@ -27,16 +29,21 @@ export default ({ _socket }: UserServices) => ({ })); }, - addWatchedAuthor: async (personcode: string) => { + addWatchedAuthor: ev(v.string())(async (personcode) => { try { await upsertAuthorUser(personcode, _socket.data.user.id, null); } catch (e) { console.log(e); return { error: "Error", errorDetails: (e as Error).message }; } - }, + }), - updateWatchedAuthor: async (data: authorUser) => { + updateWatchedAuthor: ev( + v.object({ + personcode: v.string(), + notation: v.nullable(v.number()), + }), + )(async (data) => { try { const { personcode, notation } = data; await upsertAuthorUser(personcode, _socket.data.user.id, notation); @@ -44,16 +51,16 @@ export default ({ _socket }: UserServices) => ({ console.error(e); return { error: "Error", errorDetails: (e as Error).message }; } - }, + }), - deleteWatchedAuthor: async (personcode: string) => { + deleteWatchedAuthor: ev(v.string())(async (personcode) => { await prismaDm.authorUser.deleteMany({ where: { personcode, userId: _socket.data.user.id, }, }); - }, + }), }); const upsertAuthorUser = async ( diff --git a/packages/api/services/cover-id/index.ts b/packages/api/services/cover-id/index.ts index 08d697fc6..18b24ad1f 100644 --- a/packages/api/services/cover-id/index.ts +++ b/packages/api/services/cover-id/index.ts @@ -1,35 +1,29 @@ import axios from "axios"; import https from "https"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import type { SimilarImagesResult } from "~dm-types/CoverSearchResults"; import { prismaClient as prismaCoverInfo } from "~prisma-schemas/schemas/cover_info/client"; -import type { UserServices } from "../../index"; // import { RequiredAuthMiddleware } from "../auth/util"; import { getCoverUrls } from "../coa/issue-details"; import namespaces from "../namespaces"; // import { createRateLimiter } from "../rate-limit"; import { getPastecStatus } from "../status"; -// const coverSearchRateLimiter = createRateLimiter({ -// windowMs: Number(process.env.COVER_SEARCH_RATE_LIMIT_WINDOW_MS ?? 60_000), -// max: Number(process.env.COVER_SEARCH_RATE_LIMIT_MAX ?? 60), -// }); - -const listenEvents = ({ _socket }: UserServices) => ({ - searchFromCover: async (urlOrBase64: string, pastecIndex = 0) => { - if (![0, 1].includes(pastecIndex)) { - return { error: "Invalid pastec index" } as const; - } - - // const rateLimit = coverSearchRateLimiter.check(String(_socket.data.user.id)); - // if (!rateLimit.allowed) { - // return { - // error: "Rate limit exceeded", - // retryAfterMs: rateLimit.retryAfterMs, - // } as const; - // } +const listenEvents = () => ({ + searchFromCover: ev( + v.pipe( + v.string("Invalid URL or base64 string"), + v.nonEmpty("Invalid URL or base64 string"), + ), + v.pipe( + v.optional(v.number(), 0), + v.picklist([0, 1], "Invalid pastec index" as const), + ), + )(async (urlOrBase64, pastecIndex) => { const hostAndPort = process.env.PASTEC_HOSTS_AND_PORTS!.split(",")[pastecIndex]; console.log(`Searching from cover on ${hostAndPort}`); @@ -39,7 +33,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ responseType: "arraybuffer", }) ).data - : Buffer.from(urlOrBase64.split(";base64,").pop()!, "base64"); + : Buffer.from(urlOrBase64.split(";base64,").pop() as string, "base64"); const pastecResponse = await getSimilarImages(buffer, hostAndPort); @@ -107,40 +101,43 @@ const listenEvents = ({ _socket }: UserServices) => ({ ], })), }; - }, + }), getIndexSize: async () => getPastecStatus(), - getCoverUrl: async (coverId: number) => + getCoverUrl: ev(v.number())(async (coverId) => getCoverUrl(coverId).then( (url) => `${process.env.INDUCKS_COVERS_ROOT}/${url}`, ), - - downloadCover: (coverId: number) => - new Promise((resolve) => { - getCoverUrl(coverId).then((coverUrl) => { - const data: Uint8Array[] = []; - const externalRequest = https.request( - { - hostname: process.env.INDUCKS_COVERS_ROOT, - path: coverUrl, - }, - (res) => { - res - .on("data", function (chunk) { - data.push(chunk); - }) - .on("end", function () { - //at this point data is an array of Buffers so Buffer.concat() can make us a new Buffer of all of them together - resolve({ buffer: Buffer.concat(data) }); - }); - }, - ); - externalRequest.on("error", function (err) { - console.error(err); - resolve({ error: "Error", errorDetails: err.message }); + ), + + downloadCover: ev(v.number())( + (coverId) => + new Promise((resolve) => { + getCoverUrl(coverId).then((coverUrl) => { + const data: Uint8Array[] = []; + const externalRequest = https.request( + { + hostname: process.env.INDUCKS_COVERS_ROOT, + path: coverUrl, + }, + (res) => { + res + .on("data", function (chunk) { + data.push(chunk); + }) + .on("end", function () { + //at this point data is an array of Buffers so Buffer.concat() can make us a new Buffer of all of them together + resolve({ buffer: Buffer.concat(data) }); + }); + }, + ); + externalRequest.on("error", function (err) { + console.error(err); + resolve({ error: "Error", errorDetails: err.message }); + }); + externalRequest.end(); }); - externalRequest.end(); - }); - }), + }), + ), }); export const { client, server } = useSocketEvents< diff --git a/packages/api/services/edgecreator/edge-publication/index.ts b/packages/api/services/edgecreator/edge-publication/index.ts index c5b869d88..711263b8e 100644 --- a/packages/api/services/edgecreator/edge-publication/index.ts +++ b/packages/api/services/edgecreator/edge-publication/index.ts @@ -10,16 +10,17 @@ import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import { getPopularityByIssuecodes } from "../../coa/issue-details"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default () => ({ - publishEdge: async ({ - issuecode, - designers, - photographers, - }: { - issuecode: string; - designers: string[]; - photographers: string[]; - }) => { + publishEdge: ev( + v.object({ + issuecode: v.string(), + designers: v.array(v.string()), + photographers: v.array(v.string()), + }), + )(async ({ issuecode, designers, photographers }) => { const issue = await prismaCoa.inducks_issue.findFirst({ where: { issuecode }, select: { publicationcode: true, issuenumber: true }, @@ -60,7 +61,7 @@ export default () => ({ contributors, url: `${process.env.VITE_EDGES_ROOT}/${country}/gen/${magazine}.${issuenumber.replaceAll(" ", "")}.png`, }; - }, + }), }); const getUserIdsByUsername = async ( diff --git a/packages/api/services/edgecreator/index.ts b/packages/api/services/edgecreator/index.ts index ba2b3f0c7..552e8ce4f 100644 --- a/packages/api/services/edgecreator/index.ts +++ b/packages/api/services/edgecreator/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -28,7 +30,7 @@ const listenEvents = (services: UserServices) => { ...multipleEdgePhotos(services), // TODO check if usages in SVG models - getImagesFromFilename: async (fileName: string) => + getImagesFromFilename: ev(v.string())(async (fileName) => // TODO prismaClient.edgeModel.findMany ? ( await prismaDm.$queryRaw` @@ -49,8 +51,9 @@ const listenEvents = (services: UserServices) => { .split(/\[[^]]+]/) .every((stringChunk) => fileName.indexOf(stringChunk) > -1), ), + ), - submitEdge: async (issuecode: string) => { + submitEdge: ev(v.string())(async (issuecode) => { const user = await prismaDm.user.findUniqueOrThrow({ where: { id: _socket.data.user.id, @@ -64,7 +67,7 @@ const listenEvents = (services: UserServices) => { await email.send(); return { url: email.data.ecLink }; - }, + }), }; }; diff --git a/packages/api/services/edgecreator/models/index.ts b/packages/api/services/edgecreator/models/index.ts index 89f45887c..2e458c1f8 100644 --- a/packages/api/services/edgecreator/models/index.ts +++ b/packages/api/services/edgecreator/models/index.ts @@ -3,8 +3,11 @@ import { Prisma } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import { prismaClient as prismaEdgeCreator } from "~prisma-schemas/schemas/edgecreator/client"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export default () => ({ - getModelsSteps: async (modelIds: number[]) => + getModelsSteps: ev(v.array(v.number()))(async (modelIds) => prismaEdgeCreator.$queryRaw< { issuecode: string; @@ -42,7 +45,8 @@ export default () => ({ {}, ), ), - getModel: async (issuecode: string) => { + ), + getModel: ev(v.string())(async (issuecode) => { const model = await prismaEdgeCreator.edgeModel.findFirst({ where: { issuecode, @@ -55,9 +59,9 @@ export default () => ({ }, })) > 0; return model && modelIsPublished ? model : null; - }, + }), - getModelMainPhoto: (modelId: number) => + getModelMainPhoto: ev(v.number())((modelId) => prismaEdgeCreator.elementImage.findFirstOrThrow({ select: { id: true, @@ -72,11 +76,13 @@ export default () => ({ }, }, }), + ), - getModelContributors: (modelId: number) => + getModelContributors: ev(v.number())((modelId) => prismaEdgeCreator.edgeContributor.findMany({ where: { modelId, }, }), + ), }); diff --git a/packages/api/services/edgecreator/multiple-edge-photos/index.ts b/packages/api/services/edgecreator/multiple-edge-photos/index.ts index d288c403a..b07c99e66 100644 --- a/packages/api/services/edgecreator/multiple-edge-photos/index.ts +++ b/packages/api/services/edgecreator/multiple-edge-photos/index.ts @@ -1,3 +1,5 @@ +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import dayjs from "dayjs"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -23,10 +25,9 @@ export const checkTodayLimit = (userId: number) => })); export default ({ _socket }: UserServices) => ({ - sendNewEdgePhotoEmail: async (issuecode: string) => { - if (typeof issuecode !== "string" || !issuecode) { - return { error: "Invalid issuecode" }; - } + sendNewEdgePhotoEmail: ev( + v.pipe(v.string("Invalid issuecode"), v.nonEmpty("Invalid issuecode")), + )(async (issuecode: string) => { const user = await prismaDm.user.findUniqueOrThrow({ where: { id: _socket.data.user.id }, }); @@ -38,21 +39,26 @@ export default ({ _socket }: UserServices) => ({ await email.send(); return { url: edgeUrl }; - }, - createElementImage: async (hash: string, fileName: string) => + }), + createElementImage: ev( + v.string(), + v.string(), + )(async (hash, fileName) => prismaEdgeCreator.elementImage .create({ select: { id: true }, data: { hash, fileName }, }) .then(({ id }) => ({ photoId: id })), + ), checkTodayLimit: async () => checkTodayLimit(_socket.data.user.id), - getImageByHash: async (hash: string) => + getImageByHash: ev(v.string())(async (hash) => prismaEdgeCreator.elementImage.findFirst({ where: { hash, }, }), + ), }); diff --git a/packages/api/services/edges/index.ts b/packages/api/services/edges/index.ts index 4aa817ef0..813af60fb 100644 --- a/packages/api/services/edges/index.ts +++ b/packages/api/services/edges/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -10,9 +12,6 @@ const getEdges = async (filters: { publicationcode?: string; issuecodes?: string[]; }) => { - if (!(filters.publicationcode || filters.issuecodes)) { - throw new Error("Invalid filter"); - } const issuecode = { in: filters.issuecodes || @@ -72,10 +71,23 @@ const listenEvents = () => ({ }) .then((issues) => prismaCoa.augmentIssueArrayWithInducksData(issues)), - getEdges: (filters: { publicationcode?: string; issuecodes?: string[] }) => + getEdges: ev( + v.pipe( + v.object({ + publicationcode: v.optional(v.string()), + issuecodes: v.optional(v.array(v.string())), + }), + v.check( + ({ publicationcode, issuecodes }) => + !!publicationcode || !!issuecodes?.length, + "Invalid filters" as const, + ), + ), + )((filters) => getEdges(filters) .then((edges) => prismaCoa.augmentIssueArrayWithInducksData(edges)) .then((edges) => edges.groupBy("issuecode")), + ), }); export const { client, server } = useSocketEvents( diff --git a/packages/api/services/feedback/index.ts b/packages/api/services/feedback/index.ts index 9ce69626d..f64b5a19b 100644 --- a/packages/api/services/feedback/index.ts +++ b/packages/api/services/feedback/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -7,7 +9,7 @@ import type { UserServices } from "../../index"; import namespaces from "../namespaces"; const listenEvents = ({ _socket }: UserServices) => ({ - sendFeedback: async (feedbackMessage: string) => { + sendFeedback: ev(v.string())(async (feedbackMessage) => { const user = await prismaDm.user.findUniqueOrThrow({ where: { id: _socket.data.user.id }, }); @@ -16,7 +18,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ feedbackMessage, }); await email.send(); - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/packages/api/services/global-stats/index.ts b/packages/api/services/global-stats/index.ts index e2072c07c..bd318dc72 100644 --- a/packages/api/services/global-stats/index.ts +++ b/packages/api/services/global-stats/index.ts @@ -1,4 +1,7 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + import type { QuickStatsPerUser } from "~dm-types/QuickStatsPerUser"; import { Prisma } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -119,16 +122,12 @@ const listenEvents = () => ({ }, }), - getUsersPointsAndStats: async (userIds: number[]) => - userIds.length - ? { - points: await getMedalPoints(userIds), - stats: await getUsersQuickStats(userIds), - } - : { - error: "Bad request", - errorDetails: "Empty user IDs list", - }, + getUsersPointsAndStats: ev( + v.pipe(v.array(v.number()), v.nonEmpty("Bad request" as const)), + )(async (userIds) => ({ + points: await getMedalPoints(userIds), + stats: await getUsersQuickStats(userIds), + })), }); export const { client, server } = useSocketEvents( diff --git a/packages/api/services/presentation-text/index.ts b/packages/api/services/presentation-text/index.ts index 0ee5ab500..b84f6d511 100644 --- a/packages/api/services/presentation-text/index.ts +++ b/packages/api/services/presentation-text/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -10,11 +12,11 @@ import namespaces from "../namespaces"; export type Decision = "approve" | "refuse"; const listenEvents = () => ({ - approveOrDenyPresentationText: async ( - sentence: string, - userId: number, - decision: Decision, - ) => { + approveOrDenyPresentationText: ev( + v.string(), + v.number(), + v.union([v.literal("approve"), v.literal("refuse")]), + )(async (sentence, userId, decision) => { switch (decision) { case "approve": { @@ -36,7 +38,7 @@ const listenEvents = () => ({ }), }).send(); } - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/packages/api/services/public-collection/index.ts b/packages/api/services/public-collection/index.ts index 0ae822e67..e10eba6dd 100644 --- a/packages/api/services/public-collection/index.ts +++ b/packages/api/services/public-collection/index.ts @@ -1,4 +1,6 @@ import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import type { issue, user } from "~prisma-schemas/schemas/dm"; import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; @@ -7,7 +9,7 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import namespaces from "../namespaces"; const listenEvents = () => ({ - getPublicCollection: async (username: string) => { + getPublicCollection: ev(v.string())(async (username) => { let user: user; try { user = await prismaDm.user.findFirstOrThrow({ @@ -41,7 +43,7 @@ const listenEvents = () => ({ })), ), }; - }, + }), }); export const { client, server } = useSocketEvents( diff --git a/packages/api/services/stats/suggestions.ts b/packages/api/services/stats/suggestions.ts index 6484e03e3..c3fc59c0f 100644 --- a/packages/api/services/stats/suggestions.ts +++ b/packages/api/services/stats/suggestions.ts @@ -9,17 +9,20 @@ import { prismaClient as prismaDmStats } from "~prisma-schemas/schemas/dm_stats/ import type { UserServices } from "../../index"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; + export enum COUNTRY_CODE_OPTION { ALL = "ALL", countries_to_notify = "countries_to_notify", } export default ({ _socket }: UserServices) => ({ - getSuggestionsForCountry: async ( - countrycode: string, - sincePreviousVisit: "since_previous_visit" | "_", - limit: number, - ) => { + getSuggestionsForCountry: ev( + v.string(), + v.union([v.literal("since_previous_visit"), v.literal("_")]), + v.number(), + )(async (countrycode, sincePreviousVisit, limit) => { const user = _socket.data.user; const since = sincePreviousVisit === "since_previous_visit" @@ -46,7 +49,7 @@ export default ({ _socket }: UserServices) => ({ ), ), ).then((results) => results.groupBy("sort")); - }, + }), }); type SuggestedPublications = { diff --git a/packages/api/services/story-search/index.ts b/packages/api/services/story-search/index.ts index 745833aae..01bc89a56 100644 --- a/packages/api/services/story-search/index.ts +++ b/packages/api/services/story-search/index.ts @@ -2,6 +2,8 @@ import * as fs from "fs/promises"; import { InferenceSession, Tensor } from "onnxruntime-node"; import sharp from "sharp-0-34"; import { useSocketEvents } from "socket-call-server"; +import { ev } from "socket-call-server/valibot"; +import * as v from "valibot"; import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; @@ -274,20 +276,22 @@ export const findSimilarImages = async ( }; const listenEvents = () => ({ - getIndexSize: async (isCover: boolean) => + getIndexSize: ev(v.boolean())(async (isCover) => prismaCoa.inducks_entryurl_vector.count({ where: { isCover, }, }), + ), - findSimilarImages: async ( - imageBufferOrBase64: string | Buffer, - isCover: boolean, - ) => + findSimilarImages: ev( + v.union([v.string(), v.instance(Buffer)]), + v.boolean(), + )(async (imageBufferOrBase64, isCover) => session ? findSimilarImages(imageBufferOrBase64, isCover) : ({ error: "Session not initialized" } as const), + ), }); export const { client, server } = useSocketEvents< diff --git a/packages/prisma-schemas/pnpm-lock.yaml b/packages/prisma-schemas/pnpm-lock.yaml index 4dd2a0184..a45102f15 100644 --- a/packages/prisma-schemas/pnpm-lock.yaml +++ b/packages/prisma-schemas/pnpm-lock.yaml @@ -1523,8 +1523,8 @@ snapshots: '@typescript-eslint/project-service@8.46.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.0(typescript@6.0.3) - '@typescript-eslint/types': 8.46.0 + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) + '@typescript-eslint/types': 8.70.1 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: diff --git a/packages/prisma-schemas/schemas/coa/extended/index.ts b/packages/prisma-schemas/schemas/coa/extended/index.ts index 9911652b2..0cca3ea09 100644 --- a/packages/prisma-schemas/schemas/coa/extended/index.ts +++ b/packages/prisma-schemas/schemas/coa/extended/index.ts @@ -14,7 +14,7 @@ type Augmented = { issuenumber: string; }; -export type ExtraSelectField = Exclude; +type ExtraSelectField = Exclude; export type IssuecodeDetail = Augmented & Partial>; diff --git a/packages/types/EditSubscription.ts b/packages/types/EditSubscription.ts deleted file mode 100644 index 0f818f094..000000000 --- a/packages/types/EditSubscription.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type EditSubscription = { - id: number | null; - publicationcode: string; - startDate: string; - endDate: string; -}; diff --git a/packages/types/UserForAccountForm.ts b/packages/types/UserForAccountForm.ts deleted file mode 100644 index 1e7136e47..000000000 --- a/packages/types/UserForAccountForm.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type UserForAccountForm = { - userId?: number; - oldPassword?: string; - password?: string; - password2?: string; - discordId?: string; - email: string; - allowSharing: boolean; - presentationText: string; - marketplaceAcceptsExchanges: boolean; -};