From f2e71545bddc1b3990da23a054be5b9a250e377a Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Mon, 7 Sep 2026 16:54:49 +0200 Subject: [PATCH 01/17] api: Add runtime input validation --- apps/duckguessr/api/package.json | 3 +- apps/duckguessr/api/pnpm-lock.yaml | 20 +++++-- apps/duckguessr/api/services/maintenance.ts | 16 +++--- apps/duckguessr/package.json | 2 +- apps/duckguessr/pnpm-lock.yaml | 10 ++-- apps/dumili/api/package.json | 4 +- apps/dumili/api/pnpm-lock.yaml | 27 +++++---- apps/dumili/package.json | 2 +- apps/dumili/pnpm-lock.yaml | 10 ++-- apps/edgecreator/api/package.json | 4 +- apps/edgecreator/api/pnpm-lock.yaml | 25 +++++---- apps/edgecreator/package.json | 2 +- apps/edgecreator/pnpm-lock.yaml | 10 ++-- apps/web/package.json | 2 +- apps/web/pnpm-lock.yaml | 10 ++-- apps/whattheduck/package.json | 2 +- apps/whattheduck/pnpm-lock.yaml | 24 ++++---- packages/api/package.json | 3 +- packages/api/pnpm-lock.yaml | 32 +++++++++-- packages/api/services/auth/index.ts | 55 +++++++++---------- packages/api/services/auth/util.ts | 5 -- .../api/services/coa/issue-details/index.ts | 25 +++++---- .../services/collection/marketplace/index.ts | 11 ++-- packages/api/services/collection/user/util.ts | 6 +- packages/api/services/cover-id/index.ts | 12 ++-- .../edgecreator/multiple-edge-photos/index.ts | 15 +++-- packages/api/services/edges/index.ts | 17 ++++-- 27 files changed, 207 insertions(+), 147 deletions(-) diff --git a/apps/duckguessr/api/package.json b/apps/duckguessr/api/package.json index 941ca557d..735a5aa0d 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.11.0", "socket.io": "^4.8.3", + "valibot": "^1.4.2", "~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 8184d2524..a8e4ec0a0 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.11.0 + version: 0.11.0(valibot@1.4.2(typescript@6.0.3)) socket.io: specifier: ^4.8.3 version: 4.8.3 + valibot: + specifier: ^1.4.2 + version: 1.4.2(typescript@6.0.3) ~api: specifier: workspace:* version: link:../../../packages/api @@ -1362,9 +1365,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.11.0: + resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} 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==} @@ -2815,9 +2823,11 @@ snapshots: dependencies: is-arrayish: 0.3.4 - socket-call-server@0.9.0: + socket-call-server@0.11.0(valibot@1.4.2(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.4.2(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color diff --git a/apps/duckguessr/api/services/maintenance.ts b/apps/duckguessr/api/services/maintenance.ts index 48f868bbe..89b7dbad1 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -1,9 +1,11 @@ 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 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"; @@ -23,11 +25,11 @@ const listenEvents = () => ({ group by dataset_id, decision `, - getMaintenanceDataForDataset: async ( - datasetName: string, - decisions: (entryurlDetailsDecision | "null")[], - offset: number, - ) => { + getMaintenanceDataForDataset: ev( + v.pipe(v.string()), + v.pipe(v.array(v.enum({ ...entryurlDetailsDecision, null: "null" }))), + v.pipe(v.number()), + )(async (datasetName, decisions, offset) => { if (!decisions) { throw new Error("No decisions provided"); } @@ -60,7 +62,7 @@ const listenEvents = () => ({ sitecodeUrl: "asc", }, }); - }, + }), updateMaintenanceData: async ( data: { sitecodeUrl: string; decision: entryurlDetailsDecision }[], ) => diff --git a/apps/duckguessr/package.json b/apps/duckguessr/package.json index ee10c9edc..4c9903d69 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.42", diff --git a/apps/duckguessr/pnpm-lock.yaml b/apps/duckguessr/pnpm-lock.yaml index ca2363331..9d1b52747 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.42(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.42(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.42(typescript@6.0.3)) universal-cookie: specifier: ^8.1.2 version: 8.1.2 @@ -6083,8 +6083,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 @@ -13859,7 +13859,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.42(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.42(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.42(typescript@6.0.3) diff --git a/apps/dumili/api/package.json b/apps/dumili/api/package.json index e71bd6f62..82073deec 100644 --- a/apps/dumili/api/package.json +++ b/apps/dumili/api/package.json @@ -41,8 +41,8 @@ "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.11.0", "socket.io": "^4.8.3", "unpdf": "^1.8.1", "~api": "workspace:*", diff --git a/apps/dumili/api/pnpm-lock.yaml b/apps/dumili/api/pnpm-lock.yaml index fab523e24..d5622b44a 100644 --- a/apps/dumili/api/pnpm-lock.yaml +++ b/apps/dumili/api/pnpm-lock.yaml @@ -60,11 +60,11 @@ 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.11.0 + version: 0.11.0(valibot@1.4.2(typescript@6.0.3)) socket.io: specifier: ^4.8.3 version: 4.8.3 @@ -1932,8 +1932,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 @@ -1942,9 +1942,14 @@ packages: axios-cache-interceptor: optional: true - socket-call-server@0.9.0: - resolution: {integrity: sha512-yh3KIhXlp0jaE5x/XYs+vG5sZjZbsyzTBL9jyJqa2SXJoVhnqjiqk0l6LVtxagpjeMvhTa/UgQYx1L36TShavg==} + socket-call-server@0.11.0: + resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} 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==} @@ -3970,7 +3975,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) @@ -3981,9 +3986,11 @@ snapshots: - supports-color - utf-8-validate - socket-call-server@0.9.0: + socket-call-server@0.11.0(valibot@1.4.2(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.4.2(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color diff --git a/apps/dumili/package.json b/apps/dumili/package.json index 16ad5920f..74e3b44c4 100644 --- a/apps/dumili/package.json +++ b/apps/dumili/package.json @@ -43,7 +43,7 @@ "bootstrap-vue-next": "^1.0.2", "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.42", "vue-draggable-resizable": "^3.0.0", diff --git a/apps/dumili/pnpm-lock.yaml b/apps/dumili/pnpm-lock.yaml index f3fa9ccf1..bb6a23755 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.42(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.42(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.42(typescript@6.0.3)) sortablejs: specifier: ^1.15.7 version: 1.15.7 @@ -4464,8 +4464,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 @@ -9556,7 +9556,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.42(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.42(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.42(typescript@6.0.3) diff --git a/apps/edgecreator/api/package.json b/apps/edgecreator/api/package.json index d6dde1cb7..1f13e747b 100644 --- a/apps/edgecreator/api/package.json +++ b/apps/edgecreator/api/package.json @@ -27,8 +27,8 @@ "jsdom": "^24.1.3", "jsonwebtoken": "^9.0.3", "node-base64-image": "^3.0.0", - "socket-call-client": "^0.8.0", - "socket-call-server": "^0.9.0", + "socket-call-client": "^0.10.0", + "socket-call-server": "^0.11.0", "socket.io": "^4.8.3", "universal-cookie": "^4.0.4", "~api": "workspace:*", diff --git a/apps/edgecreator/api/pnpm-lock.yaml b/apps/edgecreator/api/pnpm-lock.yaml index 6056b7e6a..bc7f15d13 100644 --- a/apps/edgecreator/api/pnpm-lock.yaml +++ b/apps/edgecreator/api/pnpm-lock.yaml @@ -48,11 +48,11 @@ importers: specifier: ^3.0.0 version: 3.0.0 socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(vue@3.5.22(typescript@6.0.3)) + specifier: ^0.10.0 + version: 0.10.0(vue@3.5.22(typescript@6.0.3)) socket-call-server: - specifier: ^0.9.0 - version: 0.9.0 + specifier: ^0.11.0 + version: 0.11.0 socket.io: specifier: ^4.8.3 version: 4.8.3 @@ -1425,8 +1425,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 @@ -1435,9 +1435,14 @@ packages: axios-cache-interceptor: optional: true - socket-call-server@0.9.0: - resolution: {integrity: sha512-yh3KIhXlp0jaE5x/XYs+vG5sZjZbsyzTBL9jyJqa2SXJoVhnqjiqk0l6LVtxagpjeMvhTa/UgQYx1L36TShavg==} + socket-call-server@0.11.0: + resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} 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==} @@ -3053,7 +3058,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) @@ -3062,7 +3067,7 @@ snapshots: - supports-color - utf-8-validate - socket-call-server@0.9.0: + socket-call-server@0.11.0: dependencies: socket.io: 4.8.3 transitivePeerDependencies: diff --git a/apps/edgecreator/package.json b/apps/edgecreator/package.json index d48e37b83..7a4e1fa44 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.103.1", - "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.42", diff --git a/apps/edgecreator/pnpm-lock.yaml b/apps/edgecreator/pnpm-lock.yaml index 8d2bc0185..e374d25d5 100644 --- a/apps/edgecreator/pnpm-lock.yaml +++ b/apps/edgecreator/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ^1.103.1 version: 1.103.1 socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.42(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.42(typescript@6.0.3)) universal-cookie: specifier: ^7.2.2 version: 7.2.2 @@ -2754,8 +2754,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 @@ -5813,7 +5813,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.42(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.42(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.42(typescript@6.0.3) diff --git a/apps/web/package.json b/apps/web/package.json index 42ee1f82e..0dc7f0478 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -35,7 +35,7 @@ "mapbox-gl": "^3.29.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.42", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index b6bb3f462..354ce85b5 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.42(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.42(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.42(typescript@6.0.3)) timeago.js: specifier: ^4.0.2 version: 4.0.2 @@ -3306,8 +3306,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 @@ -7052,7 +7052,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.42(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.20.0))(vue@3.5.42(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.42(typescript@6.0.3) diff --git a/apps/whattheduck/package.json b/apps/whattheduck/package.json index 234c99ebe..d477009da 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.103.1", - "socket-call-client": "^0.8.0", + "socket-call-client": "^0.10.0", "universal-cookie": "^7.2.2", "vue": "^3.5.42", "vue-chartjs": "^5.3.4", diff --git a/apps/whattheduck/pnpm-lock.yaml b/apps/whattheduck/pnpm-lock.yaml index 899c1e5db..47dd781ae 100644 --- a/apps/whattheduck/pnpm-lock.yaml +++ b/apps/whattheduck/pnpm-lock.yaml @@ -113,8 +113,8 @@ importers: specifier: ^1.103.1 version: 1.103.1 socket-call-client: - specifier: ^0.8.0 - version: 0.8.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.42(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.42(typescript@6.0.3)) universal-cookie: specifier: ^7.2.2 version: 7.2.2 @@ -5607,8 +5607,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 @@ -6754,7 +6754,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 @@ -7509,7 +7509,7 @@ snapshots: '@ionic/utils-array@2.1.6': dependencies: - debug: 4.3.4 + debug: 4.4.3 tslib: 2.6.2 transitivePeerDependencies: - supports-color @@ -7517,7 +7517,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: @@ -7541,7 +7541,7 @@ 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 @@ -7561,7 +7561,7 @@ snapshots: '@ionic/utils-stream@3.1.6': dependencies: - debug: 4.3.4 + debug: 4.4.3 tslib: 2.6.2 transitivePeerDependencies: - supports-color @@ -7581,7 +7581,7 @@ snapshots: '@ionic/utils-stream': 3.1.6 '@ionic/utils-terminal': 2.3.4 cross-spawn: 7.0.6 - debug: 4.3.4 + debug: 4.4.3 tslib: 2.6.2 transitivePeerDependencies: - supports-color @@ -7602,7 +7602,7 @@ 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 @@ -12413,7 +12413,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.42(typescript@6.0.3)): + socket-call-client@0.10.0(axios-cache-interceptor@1.12.3(axios@1.13.4))(vue@3.5.42(typescript@6.0.3)): dependencies: socket.io-client: 4.8.3 vue: 3.5.42(typescript@6.0.3) diff --git a/packages/api/package.json b/packages/api/package.json index f8ad4cfb3..c1bffed06 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.11.0", "socket.io": "^4.8.3", + "valibot": "^1.4.2", "~dm-types": "workspace:*", "~prisma-schemas": "workspace:*" }, diff --git a/packages/api/pnpm-lock.yaml b/packages/api/pnpm-lock.yaml index f7fcdc7b3..f9b04e259 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.11.0 + version: 0.11.0(valibot@1.4.2(typescript@6.0.3)) socket.io: specifier: ^4.8.3 version: 4.8.3 + valibot: + specifier: ^1.4.2 + version: 1.4.2(typescript@6.0.3) ~dm-types: specifier: workspace:* version: link:../types @@ -1723,9 +1726,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.11.0: + resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} 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==} @@ -1852,6 +1860,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3560,9 +3576,11 @@ snapshots: dependencies: semver: 7.8.5 - socket-call-server@0.9.0: + socket-call-server@0.11.0(valibot@1.4.2(typescript@6.0.3)): dependencies: socket.io: 4.8.3 + optionalDependencies: + valibot: 1.4.2(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -3701,6 +3719,10 @@ snapshots: util-deprecate@1.0.2: {} + valibot@1.4.2(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vary@1.1.2: {} wait-on@9.1.0: diff --git a/packages/api/services/auth/index.ts b/packages/api/services/auth/index.ts index 4f0f67875..0417998ba 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"; @@ -19,7 +21,6 @@ import namespaces from "../namespaces"; import { generateAccessToken, getHashedPassword, - isValidEmail, loginAs, } from "./util"; @@ -30,35 +31,33 @@ const listenEvents = () => ({ resolve({ error: err!.message || "" }); }); }), - requestTokenForForgotPassword: async (email: string) => { - if (!isValidEmail(email)) { - return { error: "Invalid email" }; - } else { - const user = await prismaClient.user.findFirst({ - where: { email }, + 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 }, }); - 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 }, - }); - - 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" }; - } + + 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" as const }; } - }, + }), changePassword: async ({ password, 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/coa/issue-details/index.ts b/packages/api/services/coa/issue-details/index.ts index 83d7b983a..9e43d11a2 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,11 @@ 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.config(v.pipe(v.string(), v.nonEmpty()), { + message: "Invalid issuecode", + }), + )(async (issuecode) => { const issue = await prismaCoa.inducks_issue.findFirst({ where: { issuecode }, }); @@ -47,12 +50,11 @@ 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 = ( @@ -127,7 +129,10 @@ export const getCoverUrls = async (issuecodes: string[]) => { return { issuecode, title: issue.title!, - fullUrl: getPrefixedEntryurl(coverEntryUrl.url!, coverEntryUrl.sitecode!), + fullUrl: getPrefixedEntryurl( + coverEntryUrl.url!, + coverEntryUrl.sitecode!, + ), }; }); }; diff --git a/packages/api/services/collection/marketplace/index.ts b/packages/api/services/collection/marketplace/index.ts index 15cfe30c0..77d691c33 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"; @@ -20,10 +22,9 @@ export default (services: UserServices) => { }); }, - 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,7 +58,7 @@ export default (services: UserServices) => { issueId, })), }); - }, + }), getRequests: async (as: "buyer" | "seller") => { switch (as) { diff --git a/packages/api/services/collection/user/util.ts b/packages/api/services/collection/user/util.ts index cde3600dd..70eb19b9e 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.parse(v.pipe(v.string(), v.email()), email)) { return { message: "Adresse e-mail invalide", selector: "#email", diff --git a/packages/api/services/cover-id/index.ts b/packages/api/services/cover-id/index.ts index ad4d70bc8..338870be8 100644 --- a/packages/api/services/cover-id/index.ts +++ b/packages/api/services/cover-id/index.ts @@ -1,6 +1,8 @@ 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"; @@ -10,10 +12,10 @@ import namespaces from "../namespaces"; import { getPastecStatus } from "../status"; const listenEvents = () => ({ - searchFromCover: async (urlOrBase64: string, pastecIndex = 0) => { - if (![0, 1].includes(pastecIndex)) { - return { error: "Invalid pastec index" }; - } + searchFromCover: ev(v.pipe( + v.string("Invalid URL or base64 string"), + v.nonEmpty("Invalid URL or base64 string"), + ), v.pipe(v.picklist([0, 1], "Invalid pastec index")))(async (urlOrBase64, pastecIndex) => { const hostAndPort = process.env.PASTEC_HOSTS_AND_PORTS!.split(",")[pastecIndex]; console.log(`Searching from cover on ${hostAndPort}`); const buffer = urlOrBase64.includes(";base64,") @@ -88,7 +90,7 @@ const listenEvents = () => ({ ], })), }; - }, + }), getIndexSize: async () => getPastecStatus(), getCoverUrl: async (coverId: number) => getCoverUrl(coverId).then( diff --git a/packages/api/services/edgecreator/multiple-edge-photos/index.ts b/packages/api/services/edgecreator/multiple-edge-photos/index.ts index 6584e5b33..b50a59bcb 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"; @@ -20,13 +22,14 @@ export const checkTodayLimit = (userId: number) => }) .then((data) => ({ uploadedFilesToday: data.map(({ fileName }) => fileName), - })) + })); export default ({ _socket }: UserServices) => ({ - sendNewEdgePhotoEmail: async (issuecode: string) => { - if (typeof issuecode !== "string" || !issuecode) { - return { error: "Invalid issuecode" }; - } + sendNewEdgePhotoEmail: ev( + v.config(v.pipe(v.string(), v.nonEmpty()), { + message: "Invalid issuecode", + }), + )(async (issuecode: string) => { const user = await prismaDm.user.findUniqueOrThrow({ where: { id: _socket.data.user.id }, }); @@ -38,7 +41,7 @@ export default ({ _socket }: UserServices) => ({ await email.send(); return { url: edgeUrl }; - }, + }), createElementImage: async (hash: string, fileName: string) => prismaEdgeCreator.elementImage .create({ diff --git a/packages/api/services/edges/index.ts b/packages/api/services/edges/index.ts index 4aa817ef0..f64287cbe 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,18 @@ const listenEvents = () => ({ }) .then((issues) => prismaCoa.augmentIssueArrayWithInducksData(issues)), - getEdges: (filters: { publicationcode?: string; issuecodes?: string[] }) => + getEdges: ev( + v.config( + v.object({ + publicationcode: v.optional(v.string()), + issuecodes: v.optional(v.array(v.string())), + }), + { message: "Invalid filters" }, + ), + )((filters) => getEdges(filters) .then((edges) => prismaCoa.augmentIssueArrayWithInducksData(edges)) - .then((edges) => edges.groupBy("issuecode")), + .then((edges) => edges.groupBy("issuecode"))), }); export const { client, server } = useSocketEvents( From 056ad85984cbc08de981cc6503d86c5e4378c00e Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 16 Sep 2026 15:21:44 +0200 Subject: [PATCH 02/17] Add runtime input validation --- apps/duck-estimator/cache.ts | 7 +- apps/duck-estimator/coa.ts | 2 +- apps/duck-estimator/eslint.config.mjs | 2 + apps/duck-estimator/index.ts | 16 +- apps/duck-estimator/package.json | 1 + apps/duck-estimator/pnpm-lock.yaml | 60 +++++ .../scrapes/bedetheque/check-missing.ts | 8 +- .../scrapes/bedetheque/get-revue.ts | 2 +- .../scrapes/bedetheque/index.ts | 9 +- .../scrapes/comicsmania/index.ts | 9 +- .../duck-estimator/scrapes/gocollect/index.ts | 24 +- apps/duck-estimator/scrapes/seriesam/index.ts | 13 +- apps/duckguessr/api/package.json | 4 +- apps/duckguessr/api/pnpm-lock.yaml | 28 ++- apps/duckguessr/api/services/datasets.ts | 83 ++++--- apps/duckguessr/api/services/game.ts | 10 +- apps/duckguessr/api/services/home.ts | 6 +- apps/duckguessr/api/services/maintenance.ts | 14 +- apps/duckguessr/api/services/match.ts | 6 +- apps/duckguessr/api/services/player.ts | 25 +- .../app/pages/admin/create-dataset.vue | 3 + apps/dumili/api/package.json | 3 +- apps/dumili/api/pnpm-lock.yaml | 27 ++- .../api/services/indexation-creation/index.ts | 6 +- apps/dumili/api/services/indexation/index.ts | 144 +++++++----- apps/edgecreator/api/eslint.config.mjs | 2 + apps/edgecreator/api/package.json | 4 +- apps/edgecreator/api/pnpm-lock.yaml | 87 ++++++- apps/edgecreator/api/services/browse/index.ts | 119 +++++----- .../api/services/image-info/index.ts | 7 +- apps/edgecreator/api/services/legacy/index.ts | 49 ++-- apps/edgecreator/api/services/save/index.ts | 30 ++- apps/edgecreator/api/services/text/index.ts | 159 +++++++------ apps/edgecreator/api/services/upload/index.ts | 50 ++-- .../emails/bookstore-reported-closed/index.ts | 6 +- .../api/emails/bookstore-suggested/index.ts | 2 +- packages/api/eslint.config.mjs | 2 + packages/api/http.ts | 10 +- packages/api/package.json | 5 +- packages/api/pnpm-lock.yaml | 82 ++++++- .../create-issues-from-subscriptions.ts | 7 +- .../api/scripts/import-entryurl-vectors.ts | 105 +++++---- packages/api/scripts/send-pending-emails.ts | 6 +- packages/api/services/app/index.ts | 8 +- packages/api/services/auth/index.ts | 218 +++++++++--------- packages/api/services/bookcase/index.ts | 14 +- .../api/services/bookcase/private/index.ts | 25 +- packages/api/services/bookstores/index.ts | 66 ++++-- packages/api/services/bookstores/util.ts | 22 +- packages/api/services/coa/authors/index.ts | 8 +- packages/api/services/coa/countries/index.ts | 9 +- .../api/services/coa/issue-details/index.ts | 23 +- packages/api/services/coa/issues/index.ts | 65 ++++-- .../api/services/coa/publications/index.ts | 33 ++- packages/api/services/coa/quotations/index.ts | 33 +-- packages/api/services/collection/index.ts | 58 ++--- .../api/services/collection/issues/index.ts | 140 ++++++----- .../api/services/collection/issues/util.ts | 4 +- .../api/services/collection/labels/index.ts | 18 +- .../marketplace/contact-methods/index.ts | 7 +- .../services/collection/marketplace/index.ts | 84 +++---- .../api/services/collection/options/index.ts | 19 +- .../services/collection/purchases/index.ts | 16 +- .../collection/subscriptions/index.ts | 33 ++- .../api/services/collection/user/index.ts | 18 +- .../collection/watched-authors/index.ts | 21 +- packages/api/services/cover-id/index.ts | 129 ++++++----- .../edgecreator/edge-publication/index.ts | 21 +- packages/api/services/edgecreator/index.ts | 9 +- .../api/services/edgecreator/models/index.ts | 16 +- .../edgecreator/multiple-edge-photos/index.ts | 9 +- packages/api/services/edges/index.ts | 3 +- packages/api/services/feedback/index.ts | 6 +- packages/api/services/global-stats/index.ts | 26 ++- .../api/services/presentation-text/index.ts | 16 +- .../api/services/public-collection/index.ts | 38 +-- packages/api/services/stats/suggestions.ts | 18 +- packages/api/services/story-search/index.ts | 77 ++++--- packages/types/UserForAccountForm.ts | 11 - 79 files changed, 1571 insertions(+), 994 deletions(-) delete mode 100644 packages/types/UserForAccountForm.ts diff --git a/apps/duck-estimator/cache.ts b/apps/duck-estimator/cache.ts index 5c36ade83..b9b9f6fa1 100644 --- a/apps/duck-estimator/cache.ts +++ b/apps/duck-estimator/cache.ts @@ -21,9 +21,10 @@ export const syncScrapeCache = async ( postGetFromCacheTransformFn: (buffer: Buffer) => T, preSetInCacheTransformFn: (output: T) => string | Promise, ) => { - - const error = (...args: ConsoleArgs) => console.error(`[${scrapeDirName}]`, ...args); - const debug = (...args: ConsoleArgs) => console.debug(`[${scrapeDirName}]`, ...args); + const error = (...args: ConsoleArgs) => + console.error(`[${scrapeDirName}]`, ...args); + const debug = (...args: ConsoleArgs) => + console.debug(`[${scrapeDirName}]`, ...args); const cacheDirName = `${getCacheDir()}/${scrapeDirName}`; const cacheFileName = `${cacheDirName}/${fileName}`; diff --git a/apps/duck-estimator/coa.ts b/apps/duck-estimator/coa.ts index 331d87774..1acc21b47 100644 --- a/apps/duck-estimator/coa.ts +++ b/apps/duck-estimator/coa.ts @@ -96,7 +96,7 @@ export const getInducksIssuecodesBetween = async ( }; export const getAll = () => - prismaCoa.inducks_issuequotation_raw.findMany({ + prismaCoa.inducks_issuequotation_raw.findMany({ orderBy: [ { issuecode: "asc", diff --git a/apps/duck-estimator/eslint.config.mjs b/apps/duck-estimator/eslint.config.mjs index 025d9948a..cb67de86e 100644 --- a/apps/duck-estimator/eslint.config.mjs +++ b/apps/duck-estimator/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from "@eslint/js"; import { defineConfig } from "eslint/config"; import prettierConfig from "eslint-config-prettier"; +import prettierPlugin from "eslint-plugin-prettier/recommended"; import tseslint from "typescript-eslint"; import apiRules from "../../util/lint/api-rules.mjs"; @@ -8,6 +9,7 @@ export default defineConfig( eslint.configs.recommended, tseslint.configs.recommended, prettierConfig, + prettierPlugin, { ignores: [ "**/node_modules", diff --git a/apps/duck-estimator/index.ts b/apps/duck-estimator/index.ts index edb0d6715..41e1eff43 100644 --- a/apps/duck-estimator/index.ts +++ b/apps/duck-estimator/index.ts @@ -11,7 +11,7 @@ import { scrape as comicsmania } from "./scrapes/comicsmania"; import { scrape as gocollect } from "./scrapes/gocollect"; import { scrape as seriesam } from "./scrapes/seriesam"; -export type ConsoleArgs = Parameters +export type ConsoleArgs = Parameters; const scrapes = [ { name: "bdm", scrape: bdm }, @@ -44,12 +44,12 @@ const runScrapes = async () => { await scrape(); results[index] = { status: "fulfilled", value: undefined }; console.log( - `Scrape done for ${name}, end date: ${new Date().toISOString()}` + `Scrape done for ${name}, end date: ${new Date().toISOString()}`, ); } catch (reason) { results[index] = { status: "rejected", reason }; console.error( - `Scrape failed for ${name}, end date: ${new Date().toISOString()}` + `Scrape failed for ${name}, end date: ${new Date().toISOString()}`, ); } } @@ -57,13 +57,11 @@ const runScrapes = async () => { await Promise.all( Array.from({ length: Math.min(CONCURRENT_SCRAPES, scrapes.length) }, () => - runScrapes() - ) + runScrapes(), + ), ); -const hasFailed = results.some( - (result) => result.status === "rejected" -); +const hasFailed = results.some((result) => result.status === "rejected"); await writeCsvMapping(await getAll()); exec("sh bump-dump.sh", (error, stdout, stderr) => { @@ -81,7 +79,7 @@ exec("sh bump-dump.sh", (error, stdout, stderr) => { results.forEach((result, index) => { if (result.status === "rejected") { console.error( - `Scrape failed for ${scrapes[index].name}: ${result.reason}` + `Scrape failed for ${scrapes[index].name}: ${result.reason}`, ); } }); diff --git a/apps/duck-estimator/package.json b/apps/duck-estimator/package.json index dd2c18041..4787f5720 100644 --- a/apps/duck-estimator/package.json +++ b/apps/duck-estimator/package.json @@ -35,6 +35,7 @@ "@types/yargs": "^17.0.35", "eslint": "^10.10.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", "typescript": "^6.0.3", "typescript-eslint": "^8.70.0" } diff --git a/apps/duck-estimator/pnpm-lock.yaml b/apps/duck-estimator/pnpm-lock.yaml index af2062548..47743f193 100644 --- a/apps/duck-estimator/pnpm-lock.yaml +++ b/apps/duck-estimator/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@10.10.0) + eslint-plugin-prettier: + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.7) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -135,6 +138,10 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -384,6 +391,20 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -437,6 +458,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -632,6 +656,15 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.7: + resolution: {integrity: sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==} + engines: {node: '>=14'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -676,6 +709,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -836,6 +873,8 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@pkgr/core@0.3.6': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -1125,6 +1164,15 @@ snapshots: dependencies: eslint: 10.10.0 + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.7): + dependencies: + eslint: 10.10.0 + prettier: 3.9.7 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.10.0) + eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -1199,6 +1247,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -1370,6 +1420,12 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.7: {} + punycode@2.3.1: {} qified@0.10.1: @@ -1402,6 +1458,10 @@ snapshots: strip-json-comments@3.1.1: {} + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) diff --git a/apps/duck-estimator/scrapes/bedetheque/check-missing.ts b/apps/duck-estimator/scrapes/bedetheque/check-missing.ts index 35c235a53..3f48b5bfa 100644 --- a/apps/duck-estimator/scrapes/bedetheque/check-missing.ts +++ b/apps/duck-estimator/scrapes/bedetheque/check-missing.ts @@ -58,15 +58,11 @@ try { issuecode, }, }); - log( - [issuecode, pageUrl, albumNum || "", storedTitle].join(","), - ); + log([issuecode, pageUrl, albumNum || "", storedTitle].join(",")); } } catch (_e) { warn(`Issue ${issuecode} not found`); - log( - ["?".repeat(10), pageUrl, albumNum || "", storedTitle].join(","), - ); + log(["?".repeat(10), pageUrl, albumNum || "", storedTitle].join(",")); } } } diff --git a/apps/duck-estimator/scrapes/bedetheque/get-revue.ts b/apps/duck-estimator/scrapes/bedetheque/get-revue.ts index 9e1eb9fd5..fdf01c15f 100644 --- a/apps/duck-estimator/scrapes/bedetheque/get-revue.ts +++ b/apps/duck-estimator/scrapes/bedetheque/get-revue.ts @@ -29,7 +29,7 @@ export const getRevue = async ( .body() .then((body) => body.toString()) .catch((e) => { - error(`Error while fetching ${url}: ${e}`); + error(`Error while fetching ${url}: ${e}`); throw e; }), ), diff --git a/apps/duck-estimator/scrapes/bedetheque/index.ts b/apps/duck-estimator/scrapes/bedetheque/index.ts index 2843afa17..6b9a9c051 100644 --- a/apps/duck-estimator/scrapes/bedetheque/index.ts +++ b/apps/duck-estimator/scrapes/bedetheque/index.ts @@ -14,9 +14,12 @@ import type { ConsoleArgs } from "~/index"; const MAPPING_FILE = "scrapes/bedetheque/coa-mapping.csv"; const ROOT_URL = "https://www.bedetheque.com/"; -export const error = (...args: ConsoleArgs) => console.error(`[bedetheque]`, ...args); -export const warn = (...args: ConsoleArgs) => console.warn(`[bedetheque]`, ...args); -export const log = (...args: ConsoleArgs) => console.log(`[bedetheque]`, ...args); +export const error = (...args: ConsoleArgs) => + console.error(`[bedetheque]`, ...args); +export const warn = (...args: ConsoleArgs) => + console.warn(`[bedetheque]`, ...args); +export const log = (...args: ConsoleArgs) => + console.log(`[bedetheque]`, ...args); export type CsvIssue = { bedetheque_url: string; diff --git a/apps/duck-estimator/scrapes/comicsmania/index.ts b/apps/duck-estimator/scrapes/comicsmania/index.ts index fd09b7241..735f563de 100644 --- a/apps/duck-estimator/scrapes/comicsmania/index.ts +++ b/apps/duck-estimator/scrapes/comicsmania/index.ts @@ -13,9 +13,12 @@ type CsvIssue = { issueCellRegex: string; }; -export const error = (...args: ConsoleArgs) => console.error(`[comicsmania]`, ...args); -export const log = (...args: ConsoleArgs) => console.log(`[comicsmania]`, ...args); -export const info = (...args: ConsoleArgs) => console.log(`[comicsmania]`, ...args); +export const error = (...args: ConsoleArgs) => + console.error(`[comicsmania]`, ...args); +export const log = (...args: ConsoleArgs) => + console.log(`[comicsmania]`, ...args); +export const info = (...args: ConsoleArgs) => + console.log(`[comicsmania]`, ...args); const publicationsWithIssues: CsvIssue[] = []; const quotations: Parameters[0] = []; diff --git a/apps/duck-estimator/scrapes/gocollect/index.ts b/apps/duck-estimator/scrapes/gocollect/index.ts index e73e890ea..2f57978f6 100644 --- a/apps/duck-estimator/scrapes/gocollect/index.ts +++ b/apps/duck-estimator/scrapes/gocollect/index.ts @@ -8,10 +8,14 @@ import { readCsvMapping } from "~/csv"; import { prismaClient } from "~prisma-schemas/schemas/coa/client"; import type { ConsoleArgs } from "~/index"; -export const error = (...args: ConsoleArgs) => console.error(`[gocollect]`, ...args); -export const log = (...args: ConsoleArgs) => console.log(`[gocollect]`, ...args); -export const info = (...args: ConsoleArgs) => console.log(`[gocollect]`, ...args); -export const debug = (...args: ConsoleArgs) => console.debug(`[gocollect]`, ...args); +export const error = (...args: ConsoleArgs) => + console.error(`[gocollect]`, ...args); +export const log = (...args: ConsoleArgs) => + console.log(`[gocollect]`, ...args); +export const info = (...args: ConsoleArgs) => + console.log(`[gocollect]`, ...args); +export const debug = (...args: ConsoleArgs) => + console.debug(`[gocollect]`, ...args); const MAPPING_FILE = "scrapes/gocollect/coa-mapping.csv"; const ROOT_URL = "https://gocollect.com/app/comics/"; @@ -64,9 +68,11 @@ export async function scrape() { const selector = `div.grid[wire\\:key*="grades"] a`; await page.waitForSelector(selector); const issueElementsLocator = page.locator(selector); - const issueLinks = new Set(await issueElementsLocator.evaluateAll((e) => - e.map((el) => (el as HTMLAnchorElement).href), - )).values(); + const issueLinks = new Set( + await issueElementsLocator.evaluateAll((e) => + e.map((el) => (el as HTMLAnchorElement).href), + ), + ).values(); for (const issueLinkHref of issueLinks) { log(`Scraping ${issueLinkHref}`); @@ -97,7 +103,9 @@ export async function scrape() { }, async (_contents) => { try { - await issuePage.waitForSelector(overviewSelector, { timeout: 3000 }); + await issuePage.waitForSelector(overviewSelector, { + timeout: 3000, + }); return _contents; } catch (e) { error(`Error while processing page content: ${e}`); diff --git a/apps/duck-estimator/scrapes/seriesam/index.ts b/apps/duck-estimator/scrapes/seriesam/index.ts index 24a87d59c..a788d0a29 100644 --- a/apps/duck-estimator/scrapes/seriesam/index.ts +++ b/apps/duck-estimator/scrapes/seriesam/index.ts @@ -9,10 +9,13 @@ import { import { readCsvMapping } from "~/csv"; import type { ConsoleArgs } from "~/index"; -export const error = (...args: ConsoleArgs) => console.error(`[seriesam]`, ...args); -export const warn = (...args: ConsoleArgs) => console.warn(`[seriesam]`, ...args); +export const error = (...args: ConsoleArgs) => + console.error(`[seriesam]`, ...args); +export const warn = (...args: ConsoleArgs) => + console.warn(`[seriesam]`, ...args); export const log = (...args: ConsoleArgs) => console.log(`[seriesam]`, ...args); -export const info = (...args: ConsoleArgs) => console.log(`[seriesam]`, ...args); +export const info = (...args: ConsoleArgs) => + console.log(`[seriesam]`, ...args); const MAPPING_FILE = "scrapes/seriesam/coa-mapping.csv"; const ROOT_URL = "https://www.seriesam.com/cgi-bin/guide?s="; @@ -112,9 +115,7 @@ export async function scrape() { while (true) { column = await row.$(`td:nth-child(${cellNumber})`); if (column === null) { - warn( - ` Inducks issue ${issuecodeInRange}: No quotation found`, - ); + warn(` Inducks issue ${issuecodeInRange}: No quotation found`); break; } else { const estimation = parseInt(await column.innerText()); diff --git a/apps/duckguessr/api/package.json b/apps/duckguessr/api/package.json index 9e963c5fb..b9987ee94 100644 --- a/apps/duckguessr/api/package.json +++ b/apps/duckguessr/api/package.json @@ -25,9 +25,9 @@ "jsonwebtoken": "^9.0.3", "prisma": "^7.10.0", "sharp": "^0.33.5", - "socket-call-server": "^0.11.0", + "socket-call-server": "^0.12.0", "socket.io": "^4.8.3", - "valibot": "^1.4.2", + "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 2a6b5334e..814c97a62 100644 --- a/apps/duckguessr/api/pnpm-lock.yaml +++ b/apps/duckguessr/api/pnpm-lock.yaml @@ -33,14 +33,14 @@ importers: specifier: ^0.33.5 version: 0.33.5 socket-call-server: - specifier: ^0.11.0 - version: 0.11.0(valibot@1.4.2(typescript@6.0.3)) + 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.4.2 - version: 1.4.2(typescript@6.0.3) + specifier: ^1.5.0 + version: 1.5.0(typescript@6.0.3) ~api: specifier: workspace:* version: link:../../../packages/api @@ -1395,8 +1395,8 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} - socket-call-server@0.11.0: - resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} peerDependencies: valibot: ^1.0.0 @@ -1488,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'} @@ -2893,11 +2901,11 @@ snapshots: dependencies: is-arrayish: 0.3.4 - socket-call-server@0.11.0(valibot@1.4.2(typescript@6.0.3)): + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 optionalDependencies: - valibot: 1.4.2(typescript@6.0.3) + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -2995,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 cea0a7f2c..16dcdf75f 100644 --- a/apps/duckguessr/api/services/datasets.ts +++ b/apps/duckguessr/api/services/datasets.ts @@ -7,6 +7,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"; export type DatasetsServices = NamespaceProxyTarget< Socket, @@ -33,36 +35,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.union([v.array(v.string()), v.undefined()]), + oldestDateFilterMin: v.union([v.number(), v.undefined()]), + oldestDateFilterMax: v.union([v.number(), v.undefined()]), + }), + 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) @@ -96,19 +96,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 134951724..3c11e85f3 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 1d41396fe..953c4a780 100644 --- a/apps/duckguessr/api/services/home.ts +++ b/apps/duckguessr/api/services/home.ts @@ -3,6 +3,8 @@ import { type NamespaceProxyTarget, 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"; export type HomeServices = NamespaceProxyTarget< Socket, @@ -28,7 +30,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, @@ -72,7 +74,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 89b7dbad1..1657635bc 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -63,9 +63,16 @@ const listenEvents = () => ({ }, }); }), - updateMaintenanceData: async ( - data: { sitecodeUrl: string; decision: entryurlDetailsDecision }[], - ) => + updateMaintenanceData: ev( + v.pipe( + v.array( + v.object({ + sitecodeUrl: v.string(), + decision: v.enum(entryurlDetailsDecision), + }), + ), + ), + )(async (data) => prisma.$transaction( data.map(({ sitecodeUrl, decision }) => prisma.entryurlDetails.update({ @@ -79,6 +86,7 @@ const listenEvents = () => ({ }), ), ), + ), }); const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/api/services/match.ts b/apps/duckguessr/api/services/match.ts index 47c8e2b4d..d94cba292 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"; export type MatchServices = NamespaceProxyTarget< Socket< @@ -20,12 +22,12 @@ export 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 2c7ef9a45..3026fb83c 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; @@ -24,15 +27,21 @@ export type PlayerServices = NamespaceProxyTarget< Record >; +const playerValidation = v.object({ + id: v.number(), + username: v.string(), + ducksmanagerId: v.nullable(v.number()), + avatar: v.string(), +}); + const listenEvents = ({ _socket }: PlayerServices) => ({ - getPlayer: () => { - return Promise.resolve(_socket.data.user); - }, + getPlayer: () => Promise.resolve(_socket.data.user), - updateUser: async (updatedPlayer: player) => + updateUser: ev(playerValidation)(async (updatedPlayer) => updatePlayer(updatedPlayer.id, updatedPlayer), + ), - getStats: async (gameId?: number) => { + getStats: ev(v.optional(v.number()))(async (gameId) => { const playerIdsToQuery = [_socket.data.user.id]; if (gameId) { playerIdsToQuery.push( @@ -46,9 +55,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 +72,7 @@ const listenEvents = ({ _socket }: PlayerServices) => ({ } const stats = await getPlayerStatistics(playerIdsToQuery); return { gameId, stats }; - }, + }), }); const { client, server } = useSocketEvents< diff --git a/apps/duckguessr/app/pages/admin/create-dataset.vue b/apps/duckguessr/app/pages/admin/create-dataset.vue index 50d29d2cf..7706b7008 100644 --- a/apps/duckguessr/app/pages/admin/create-dataset.vue +++ b/apps/duckguessr/app/pages/admin/create-dataset.vue @@ -489,6 +489,9 @@ watch( .then((result) => { datasetPreview.value = result; }) + .catch((error) => { + console.error("Error previewing dataset:", error); + }) .finally(() => { isCalculatingDatasetPreview.value = false; }); diff --git a/apps/dumili/api/package.json b/apps/dumili/api/package.json index 108092793..d217dfa2d 100644 --- a/apps/dumili/api/package.json +++ b/apps/dumili/api/package.json @@ -42,9 +42,10 @@ "pdfjs-dist": "^6.3.289", "sharp": "^0.33.5", "socket-call-client": "^0.10.0", - "socket-call-server": "^0.11.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 83f0f176c..fbe520512 100644 --- a/apps/dumili/api/pnpm-lock.yaml +++ b/apps/dumili/api/pnpm-lock.yaml @@ -63,14 +63,17 @@ importers: 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.11.0 - version: 0.11.0(valibot@1.4.2(typescript@6.0.3)) + 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 @@ -1972,8 +1975,8 @@ packages: axios-cache-interceptor: optional: true - socket-call-server@0.11.0: - resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} peerDependencies: valibot: ^1.0.0 @@ -2098,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'} @@ -4056,11 +4067,11 @@ snapshots: - supports-color - utf-8-validate - socket-call-server@0.11.0(valibot@1.4.2(typescript@6.0.3)): + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 optionalDependencies: - valibot: 1.4.2(typescript@6.0.3) + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -4183,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 6935779fc..fa09fcfeb 100644 --- a/apps/dumili/api/services/indexation-creation/index.ts +++ b/apps/dumili/api/services/indexation-creation/index.ts @@ -10,13 +10,16 @@ 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) => + create: ev(v.number())(async (numberOfPages) => prisma.indexation .create({ data: { @@ -44,6 +47,7 @@ const listenEvents = ({ _socket }: IndexationCreationServices) => ({ }), ) .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..f92f31ae5 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,15 @@ const listenEvents = (services: IndexationServices) => ({ status: "OK", }; }); - }, + }), - createStorySuggestion: async ( - suggestion: Prisma.storySuggestionUncheckedCreateInput, - ) => + createStorySuggestion: ev( + v.object({ + storycode: v.string(), + title: v.string(), + entryId: v.number(), + }), + )(async (suggestion) => prisma.storySuggestion .create({ data: suggestion, @@ -1080,13 +1088,16 @@ const listenEvents = (services: IndexationServices) => ({ createdStorySuggestion, }; }), - - createIssueSuggestion: async ( - suggestion: Omit< - Prisma.issueSuggestionUncheckedCreateInput, - "indexationId" - >, - ) => + ), + + createIssueSuggestion: ev( + v.object({ + title: v.string(), + description: v.string(), + publicationcode: v.string(), + issuenumber: v.string(), + }), + )(async (suggestion) => prisma.issueSuggestion .create({ data: { @@ -1124,25 +1135,31 @@ 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( + (indexation) => + !("releaseDate" in indexation) || + indexation.releaseDate === null || + !!new Date(indexation.releaseDate), + "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 +1200,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 +1229,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 +1266,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.optional(v.string()), + position: v.optional(v.number()), + }), + )(async (entryId, data) => { const entry = services._socket.data.indexation.entries.find( ({ id }) => id === entryId, ); @@ -1275,12 +1296,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 +1317,7 @@ const listenEvents = (services: IndexationServices) => ({ status: "OK", }; }), + ), }); export const { client, server } = useSocketEvents< diff --git a/apps/edgecreator/api/eslint.config.mjs b/apps/edgecreator/api/eslint.config.mjs index f4e1f2e31..007278f29 100644 --- a/apps/edgecreator/api/eslint.config.mjs +++ b/apps/edgecreator/api/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from "@eslint/js"; import { defineConfig } from "eslint/config"; import prettierConfig from "eslint-config-prettier"; +import prettierPlugin from "eslint-plugin-prettier/recommended"; import tseslint from "typescript-eslint"; import apiRules from "../../../util/lint/api-rules.mjs"; @@ -8,6 +9,7 @@ export default defineConfig( eslint.configs.recommended, tseslint.configs.recommended, prettierConfig, + prettierPlugin, { ignores: [ "**/node_modules", diff --git a/apps/edgecreator/api/package.json b/apps/edgecreator/api/package.json index ebf5b3c4b..0e675ff48 100644 --- a/apps/edgecreator/api/package.json +++ b/apps/edgecreator/api/package.json @@ -28,9 +28,10 @@ "jsonwebtoken": "^9.0.3", "node-base64-image": "^3.0.0", "socket-call-client": "^0.10.0", - "socket-call-server": "^0.11.0", + "socket-call-server": "^0.12.0", "socket.io": "^4.8.3", "universal-cookie": "^4.0.4", + "valibot": "^1.5.0", "~api": "workspace:*", "~dm-types": "workspace:*", "~prisma-schemas": "workspace:*" @@ -46,6 +47,7 @@ "concurrently": "^10.0.5", "eslint": "^10.10.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", "typescript": "^6.0.3", "typescript-eslint": "^8.70.0" } diff --git a/apps/edgecreator/api/pnpm-lock.yaml b/apps/edgecreator/api/pnpm-lock.yaml index bac50ae6f..9b239526e 100644 --- a/apps/edgecreator/api/pnpm-lock.yaml +++ b/apps/edgecreator/api/pnpm-lock.yaml @@ -51,14 +51,17 @@ importers: specifier: ^0.10.0 version: 0.10.0(vue@3.5.22(typescript@6.0.3)) socket-call-server: - specifier: ^0.11.0 - version: 0.11.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 universal-cookie: specifier: ^4.0.4 version: 4.0.4 + valibot: + specifier: ^1.5.0 + version: 1.5.0(typescript@6.0.3) ~api: specifier: workspace:* version: link:../../../packages/api @@ -99,6 +102,9 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@10.10.0) + eslint-plugin-prettier: + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.7) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -297,6 +303,10 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + '@sentry/conventions@0.16.0': resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} engines: {node: '>=14'} @@ -856,6 +866,20 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -920,6 +944,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1330,6 +1357,15 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.7: + resolution: {integrity: sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==} + engines: {node: '>=14'} + hasBin: true + proxy-addr@2.0.8: resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} @@ -1462,8 +1498,8 @@ packages: axios-cache-interceptor: optional: true - socket-call-server@0.11.0: - resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} peerDependencies: valibot: ^1.0.0 @@ -1516,6 +1552,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -1589,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'} @@ -1873,6 +1921,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} + '@pkgr/core@0.3.6': {} + '@sentry/conventions@0.16.0': {} '@sentry/core@10.74.0': @@ -2465,6 +2515,15 @@ snapshots: dependencies: eslint: 10.10.0 + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.7): + dependencies: + eslint: 10.10.0 + prettier: 3.9.7 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.10.0) + eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -2579,6 +2638,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -2988,6 +3049,12 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.7: {} + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 @@ -3132,9 +3199,11 @@ snapshots: - supports-color - utf-8-validate - socket-call-server@0.11.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 @@ -3205,6 +3274,10 @@ snapshots: symbol-tree@3.2.4: {} + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) @@ -3275,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 8662ab420..8a30253ec 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"; @@ -24,26 +26,28 @@ const parser = new XMLParser({ const getSvgMetadata = ( metadataNodes: { "#text": string; type?: string }[], - metadataType: string + metadataType: string, ) => metadataNodes .filter( ({ type, "#text": text }) => - type === metadataType && typeof text === "string" + type === metadataType && typeof text === "string", ) .map(({ "#text": text }) => text.trim()); const findPublishedEdges = async (publicationcode: string) => { const [countrycode, magazinecode] = publicationcode.split("/"); - const coaIssues = (await prismaCoa.inducks_issue.findMany({ - select: { - issuecode: true, - issuenumber: true, - }, - where: { - publicationcode, - }, - })).groupBy('issuecode', 'issuenumber'); + const coaIssues = ( + await prismaCoa.inducks_issue.findMany({ + select: { + issuecode: true, + issuenumber: true, + }, + where: { + publicationcode, + }, + }) + ).groupBy("issuecode", "issuenumber"); const existingEdges = ( await prismaDm.edge.findMany({ select: { @@ -52,14 +56,16 @@ const findPublishedEdges = async (publicationcode: string) => { }, where: { issuecode: { - in: Object.keys(coaIssues) + in: Object.keys(coaIssues), }, }, }) - ).map((issue) => ({ - ...issue, - fileName: `${publicationcode.split('/')[1]}.${coaIssues[issue.issuecode].replace(/[ ]+/, "")}.png`, - })).groupBy("fileName"); + ) + .map((issue) => ({ + ...issue, + fileName: `${publicationcode.split("/")[1]}.${coaIssues[issue.issuecode].replace(/[ ]+/, "")}.png`, + })) + .groupBy("fileName"); const genDir = `${getEdgesPath()}/${countrycode}/gen`; if (!existsSync(genDir)) { @@ -103,7 +109,7 @@ const findOngoingEdges = async (currentUsername?: string) => { }) .filter( (file) => - file.isDirectory() && existsSync(`${getEdgesPath()}/${file.name}/gen`) + file.isDirectory() && existsSync(`${getEdgesPath()}/${file.name}/gen`), ) .flatMap((countryDir) => { const genDir = `${getEdgesPath()}/${countryDir.name}/gen`; @@ -130,11 +136,11 @@ const findOngoingEdges = async (currentUsername?: string) => { const designers = getSvgMetadata( metadataNodes, - "contributor-designer" + "contributor-designer", ); const photographers = getSvgMetadata( metadataNodes, - "contributor-photographer" + "contributor-photographer", ); return { @@ -188,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"; @@ -231,31 +239,28 @@ 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( - `${getEdgesPath()}/${country}/${imageType}` + `${getEdgesPath()}/${country}/${imageType}`, ).filter((item) => - new RegExp(`(?:^|[. ])${magazine}(?:[. ]|$)`).test(item) + new RegExp(`(?:^|[. ])${magazine}(?:[. ]|$)`).test(item), ), }; } 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..6eac16329 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.pipe(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 6646dbbd2..d2ddcdebc 100644 --- a/apps/edgecreator/api/services/legacy/index.ts +++ b/apps/edgecreator/api/services/legacy/index.ts @@ -3,14 +3,20 @@ import { useSocketEvents } from "socket-call-server"; import type { Socket } from "socket.io"; 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.object({ + publicationcode: v.string(), + issuenumber: 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) @@ -24,17 +30,23 @@ 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.object({ + publicationcode: v.string(), + issuenumber: 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}) @@ -59,6 +71,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 f0d9106b6..0aac0686e 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 e7a46ddf5..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); @@ -40,9 +42,7 @@ const fetchSessionHash = async (parameters: { font: string }) => { if (sessionHashMatch) { sessionHashes[parameters.font] = sessionHashMatch[0]; } else { - throw new Error( - `No session ID found in URL ${url}, regex: ${regex}`, - ); + throw new Error(`No session ID found in URL ${url}, regex: ${regex}`); } }; @@ -82,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 08ad2934c..7eb79a890 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"; @@ -19,7 +21,7 @@ import { checkTodayLimit } from "~dm-services/edgecreator/multiple-edge-photos"; const getEdgeCreatorServices = (token: string) => new SocketClient( - process.env.DM_SOCKET_URL! + process.env.DM_SOCKET_URL!, ).addNamespace(namespaces.EDGECREATOR, { session: { getToken: () => Promise.resolve(token), @@ -30,7 +32,7 @@ const getEdgeCreatorServices = (token: string) => const hasReachedDailyUploadLimit = (userId: number) => checkTodayLimit(userId).then( - ({ uploadedFilesToday }) => uploadedFilesToday.length > 10 + ({ uploadedFilesToday }) => uploadedFilesToday.length > 10, ); const hasAlreadySentPhoto = async (hash: string, token: string) => @@ -47,7 +49,7 @@ const calculateHash = (data: string) => { const _getFilenameUsagesInOtherModels = async ( filename: string, currentIssuecode: string, - token: string + token: string, ) => { const issue = await prismaCoa.inducks_issue.findFirstOrThrow({ where: { issuecode: currentIssuecode }, @@ -65,7 +67,7 @@ const validateUpload = async ( isEdgePhoto: boolean, filePath: string, token: string, - userId: number + userId: number, ) => { const hash = calculateHash(filePath); if (await hasAlreadySentPhoto(hash, token)) { @@ -123,7 +125,7 @@ const getTargetFilePath = async ({ if (isEdgePhoto) { filePath = getNextAvailableFile( `${filePath}/photos/${magazinecode}.${issuenumber}.photo`, - "jpg" + "jpg", ); } else { fileName = fileName!.normalize("NFD").replace(/[\u0300-\u036F]/g, ""); @@ -142,27 +144,27 @@ export 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; + uploadFromBase64: ev( + v.union([ + v.object({ + data: v.string(), + issuecode: v.string(), + isEdgePhoto: v.literal(false), + fileName: v.string(), + }), + v.object({ + data: v.string(), + issuecode: v.string(), + isEdgePhoto: v.literal(true), + fileName: v.undefined(), + }), + ]), + )(async ({ issuecode, data, isEdgePhoto, fileName }) => { const cleanData = data.includes(",") ? data.split(",")[1] : data; const targetFilePath = await getTargetFilePath( isEdgePhoto ? { issuecode, isEdgePhoto } - : { issuecode, isEdgePhoto, fileName } + : { issuecode, isEdgePhoto, fileName }, ); const token = socket.data.user!.token; @@ -173,7 +175,7 @@ const listenEvents = ({ _socket: socket }: UploadServices) => ({ isEdgePhoto, cleanData, token, - socket.data.user!.id + socket.data.user!.id, ); console.log("validationResults", validationResults); @@ -197,7 +199,7 @@ const listenEvents = ({ _socket: socket }: UploadServices) => ({ } return { fileName: targetFileName }; - }, + }), }); export const { client, server } = useSocketEvents< diff --git a/packages/api/emails/bookstore-reported-closed/index.ts b/packages/api/emails/bookstore-reported-closed/index.ts index 1f6f3f5c7..77db1ff0d 100644 --- a/packages/api/emails/bookstore-reported-closed/index.ts +++ b/packages/api/emails/bookstore-reported-closed/index.ts @@ -8,7 +8,11 @@ export default class extends Email { ? import.meta.dirname : `/app/emails/bookstore-reported-closed`; - constructor(data: { username: string, bookstoreId: number, bookstoreName: string }) { + constructor(data: { + username: string; + bookstoreId: number; + bookstoreName: string; + }) { super(); this.data = data; } diff --git a/packages/api/emails/bookstore-suggested/index.ts b/packages/api/emails/bookstore-suggested/index.ts index e42287f2a..a6f611f2d 100644 --- a/packages/api/emails/bookstore-suggested/index.ts +++ b/packages/api/emails/bookstore-suggested/index.ts @@ -17,7 +17,7 @@ export default class extends Email { getTo = () => process.env.SMTP_USERNAME!; getToName = () => process.env.SMTP_FRIENDLYNAME!; - getFrom = () => this.data.user?.email || 'anonymous@ducksmanager.net'; + getFrom = () => this.data.user?.email || "anonymous@ducksmanager.net"; getFromName = () => this.data.user?.username || "Anonymous"; getSubject = () => "Bookstore suggestion"; } diff --git a/packages/api/eslint.config.mjs b/packages/api/eslint.config.mjs index 7e01737cb..1b639a8aa 100644 --- a/packages/api/eslint.config.mjs +++ b/packages/api/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from "@eslint/js"; import { defineConfig } from "eslint/config"; import prettierConfig from "eslint-config-prettier"; +import prettierPlugin from "eslint-plugin-prettier/recommended"; import tseslint from "typescript-eslint"; import apiRules from "../../util/lint/api-rules.mjs"; @@ -8,6 +9,7 @@ export default defineConfig( eslint.configs.recommended, tseslint.configs.recommended, prettierConfig, + prettierPlugin, { ignores: [ "**/node_modules", diff --git a/packages/api/http.ts b/packages/api/http.ts index acacbd98d..bc11faf88 100644 --- a/packages/api/http.ts +++ b/packages/api/http.ts @@ -30,14 +30,14 @@ export default () => data = dbStatus; break; } - + case "/status/pastecsearch": case "/status/pastecsearch/0": - data = await getPastecSearchStatus(0); - break; + data = await getPastecSearchStatus(0); + break; case "/status/pastecsearch/1": - data = await getPastecSearchStatus(1); - break; + data = await getPastecSearchStatus(1); + break; case "/status/pastec": data = await getPastecStatus(); diff --git a/packages/api/package.json b/packages/api/package.json index d311ec4c7..9d6305344 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -38,9 +38,9 @@ "nodemailer": "^8.0.11", "onnxruntime-node": "1.22.0-rev", "sharp-0-34": "npm:sharp@0.34.5", - "socket-call-server": "^0.11.0", + "socket-call-server": "^0.12.0", "socket.io": "^4.8.3", - "valibot": "^1.4.2", + "valibot": "^1.5.0", "~dm-types": "workspace:*", "~prisma-schemas": "workspace:*" }, @@ -57,6 +57,7 @@ "concurrently": "^10.0.5", "eslint": "^10.10.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", "nodemon": "^3.1.14", "typescript": "^6.0.3", "typescript-eslint": "^8.70.0", diff --git a/packages/api/pnpm-lock.yaml b/packages/api/pnpm-lock.yaml index cd8f3c76c..33cec23ae 100644 --- a/packages/api/pnpm-lock.yaml +++ b/packages/api/pnpm-lock.yaml @@ -66,14 +66,14 @@ importers: specifier: npm:sharp@0.34.5 version: sharp@0.34.5 socket-call-server: - specifier: ^0.11.0 - version: 0.11.0(valibot@1.4.2(typescript@6.0.3)) + 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.4.2 - version: 1.4.2(typescript@6.0.3) + specifier: ^1.5.0 + version: 1.5.0(typescript@6.0.3) ~dm-types: specifier: workspace:* version: link:../types @@ -117,6 +117,9 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@10.10.0) + eslint-plugin-prettier: + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.7) nodemon: specifier: ^3.1.14 version: 3.1.14 @@ -495,6 +498,10 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + '@pusher/push-notifications-server@1.2.7': resolution: {integrity: sha512-V/jFCSnOVqiVefzBsRur51PUOBYuDAD95bQVUQFQiCHI/LiyQcnE/w0DIzNR6OgOnJmmiuxyTmFccf06KHxZ+A==} @@ -1103,6 +1110,20 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -1156,6 +1177,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1620,6 +1644,15 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.7: + resolution: {integrity: sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==} + engines: {node: '>=14'} + hasBin: true + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -1753,8 +1786,8 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - socket-call-server@0.11.0: - resolution: {integrity: sha512-JtVUSZ2iwhmXcflqLplfg3i63Av8HZErJcrEPxic+2WI0WKNQnsZtyDfDO3d+oIMz74DvUHN1Qsb/3+4zoJ4dg==} + socket-call-server@0.12.0: + resolution: {integrity: sha512-3gJP+03tspzEfLMak8BRRuVJFuWAKsZHatN74rKDPp4jLNpRim6p8MALx9Qbk3B8n1muXl4tMzYrroHa4of4ig==} engines: {node: '>=22.0.0'} peerDependencies: valibot: ^1.0.0 @@ -1818,6 +1851,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -1887,8 +1924,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - valibot@1.4.2: - resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -2335,6 +2372,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} + '@pkgr/core@0.3.6': {} + '@pusher/push-notifications-server@1.2.7': dependencies: jsonwebtoken: 9.0.3 @@ -2981,6 +3020,15 @@ snapshots: dependencies: eslint: 10.10.0 + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.7): + dependencies: + eslint: 10.10.0 + prettier: 3.9.7 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.10.0) + eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -3055,6 +3103,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -3501,6 +3551,12 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.7: {} + progress@2.0.3: {} proxy-from-env@1.1.0: {} @@ -3641,11 +3697,11 @@ snapshots: dependencies: semver: 7.8.5 - socket-call-server@0.11.0(valibot@1.4.2(typescript@6.0.3)): + socket-call-server@0.12.0(valibot@1.5.0(typescript@6.0.3)): dependencies: socket.io: 4.8.3 optionalDependencies: - valibot: 1.4.2(typescript@6.0.3) + valibot: 1.5.0(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -3723,6 +3779,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -3784,7 +3844,7 @@ snapshots: util-deprecate@1.0.2: {} - valibot@1.4.2(typescript@6.0.3): + valibot@1.5.0(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 diff --git a/packages/api/scripts/create-issues-from-subscriptions.ts b/packages/api/scripts/create-issues-from-subscriptions.ts index cd3203af6..e228a8af2 100644 --- a/packages/api/scripts/create-issues-from-subscriptions.ts +++ b/packages/api/scripts/create-issues-from-subscriptions.ts @@ -76,12 +76,11 @@ for (const subscription of ongoingSubscriptions) { await prismaDm.subscriptionRelease.create({ data: { issuecode: release.issuecode, - releaseDate: new Date(release.filledoldestdate), + releaseDate: new Date(release.filledoldestdate), publicationcode: release.publicationcode, }, }); - } - else { + } else { console.log( "Issue %s already exists for user %s", release.issuecode, @@ -118,4 +117,4 @@ await prismaDm.subscriptionRelease.updateMany({ console.log("Done"); -await disconnectAllClients(); \ No newline at end of file +await disconnectAllClients(); diff --git a/packages/api/scripts/import-entryurl-vectors.ts b/packages/api/scripts/import-entryurl-vectors.ts index 85807ae39..88f71282b 100644 --- a/packages/api/scripts/import-entryurl-vectors.ts +++ b/packages/api/scripts/import-entryurl-vectors.ts @@ -34,10 +34,14 @@ const files = readdirSync(root, { const RECALCULATE_ALL = process.env.RECALCULATE_ALL === "true"; if (RECALCULATE_ALL) { - console.log("⚠️ RECALCULATE_ALL=true: Will recalculate all vectors (existing vectors will be replaced)"); + console.log( + "⚠️ RECALCULATE_ALL=true: Will recalculate all vectors (existing vectors will be replaced)", + ); } else { const existingVectorCount = await prismaCoa.inducks_entryurl_vector.count(); - console.log(`Found ${existingVectorCount} existing vectors (will skip these)`); + console.log( + `Found ${existingVectorCount} existing vectors (will skip these)`, + ); } const filesToProcess: { @@ -114,12 +118,14 @@ const processFiles = async (tableName: string) => { return; } - const filesWithEntriesUrls = new Set(filesWithEntries.map(f => f.url)); - const allFiles = await prismaCoa.$queryRawUnsafe<{ url: string; relativePath: string }[]>(` + const filesWithEntriesUrls = new Set(filesWithEntries.map((f) => f.url)); + const allFiles = await prismaCoa.$queryRawUnsafe< + { url: string; relativePath: string }[] + >(` SELECT url, relative_path AS relativePath FROM ${tableName} `); - + for (const file of allFiles) { if (!filesWithEntriesUrls.has(file.url)) { console.log(`Entry not found for ${file.relativePath}`); @@ -131,9 +137,13 @@ const processFiles = async (tableName: string) => { for (let i = 0; i < filesWithEntries.length; i += PROCESS_BATCH_SIZE) { const batch = filesWithEntries.slice(i, i + PROCESS_BATCH_SIZE); const batchNumber = Math.floor(i / PROCESS_BATCH_SIZE) + 1; - const totalBatches = Math.ceil(filesWithEntries.length / PROCESS_BATCH_SIZE); - - console.log(`Processing batch ${batchNumber}/${totalBatches} (${batch.length} files)`); + const totalBatches = Math.ceil( + filesWithEntries.length / PROCESS_BATCH_SIZE, + ); + + console.log( + `Processing batch ${batchNumber}/${totalBatches} (${batch.length} files)`, + ); const vectorPromises = batch.map(async (item) => { try { @@ -151,9 +161,7 @@ const processFiles = async (tableName: string) => { const vectorString = formatVectorForDB(vector.vector); if (!vector.vector || vector.vector.length === 0) { - console.error( - `Empty vector generated for ${item.relativePath}`, - ); + console.error(`Empty vector generated for ${item.relativePath}`); return null; } @@ -164,7 +172,8 @@ const processFiles = async (tableName: string) => { relativePath: item.relativePath, }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); console.error( `Error creating image vector for ${item.relativePath}: ${errorMessage}`, ); @@ -185,47 +194,58 @@ const processFiles = async (tableName: string) => { continue; } - await prismaCoa.$transaction(async (tx) => { - const escapeSqlString = (str: string) => str.replace(/\\/g, "\\\\").replace(/'/g, "''"); + await prismaCoa + .$transaction(async (tx) => { + const escapeSqlString = (str: string) => + str.replace(/\\/g, "\\\\").replace(/'/g, "''"); - if (RECALCULATE_ALL) { - for (const vector of validVectors) { - const entrycodeEscaped = escapeSqlString(vector.entrycode); - const vectorStringEscaped = escapeSqlString(vector.vectorString); - - await tx.$executeRawUnsafe(` + if (RECALCULATE_ALL) { + for (const vector of validVectors) { + const entrycodeEscaped = escapeSqlString(vector.entrycode); + const vectorStringEscaped = escapeSqlString(vector.vectorString); + + await tx.$executeRawUnsafe(` INSERT INTO inducks_entryurl_vector (entrycode, v, is_cover) VALUES ('${entrycodeEscaped}', VEC_FromText('${vectorStringEscaped}'), ${vector.isCover}) ON DUPLICATE KEY UPDATE v = VALUES(v), is_cover = VALUES(is_cover) `); - } - } else { - for (let j = 0; j < validVectors.length; j += VECTOR_INSERT_BATCH_SIZE) { - const vectorBatch = validVectors.slice(j, j + VECTOR_INSERT_BATCH_SIZE); - const values = vectorBatch - .map( - (v) => { + } + } else { + for ( + let j = 0; + j < validVectors.length; + j += VECTOR_INSERT_BATCH_SIZE + ) { + const vectorBatch = validVectors.slice( + j, + j + VECTOR_INSERT_BATCH_SIZE, + ); + const values = vectorBatch + .map((v) => { const entrycodeEscaped = escapeSqlString(v.entrycode); const vectorStringEscaped = escapeSqlString(v.vectorString); return `('${entrycodeEscaped}', VEC_FromText('${vectorStringEscaped}'), ${v.isCover})`; - } - ) - .join(", "); + }) + .join(", "); - await tx.$executeRawUnsafe(` + await tx.$executeRawUnsafe(` INSERT INTO inducks_entryurl_vector (entrycode, v, is_cover) VALUES ${values} `); + } } - } - totalProcessed += validVectors.length; - console.log( - `Successfully inserted ${validVectors.length} vectors (total: ${totalProcessed})`, - ); - }).catch((error) => { - console.error(`Error inserting vectors from batch ${batchNumber}:`, error); - }); + totalProcessed += validVectors.length; + console.log( + `Successfully inserted ${validVectors.length} vectors (total: ${totalProcessed})`, + ); + }) + .catch((error) => { + console.error( + `Error inserting vectors from batch ${batchNumber}:`, + error, + ); + }); } }; @@ -240,7 +260,8 @@ await prismaCoa.$transaction( ) `); - const escapeSqlString = (str: string) => str.replace(/\\/g, "\\\\").replace(/'/g, "''"); + const escapeSqlString = (str: string) => + str.replace(/\\/g, "\\\\").replace(/'/g, "''"); let insertedCount = 0; @@ -266,8 +287,8 @@ await prismaCoa.$transaction( console.log(`Inserted ${insertedCount} files into table ${tableName}`); }, { - timeout: 30000 - } + timeout: 30000, + }, ); try { diff --git a/packages/api/scripts/send-pending-emails.ts b/packages/api/scripts/send-pending-emails.ts index c62681921..8b2240c43 100644 --- a/packages/api/scripts/send-pending-emails.ts +++ b/packages/api/scripts/send-pending-emails.ts @@ -57,9 +57,9 @@ for (const contributionType of Object.keys( ].totalPoints; const pointsEarned = finalPointsCount - initialPointsCount; - const medalReached = Object.entries( - medalLevels[contributionType], - ).reduce( + const medalReached = Object.entries(medalLevels[contributionType]).reduce< + number | null + >( (medalReached, [medal, medalThreshold]) => initialPointsCount < medalThreshold && finalPointsCount >= medalThreshold diff --git a/packages/api/services/app/index.ts b/packages/api/services/app/index.ts index b8c1f1ae6..ca015241a 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,11 @@ export const getUpdateFileUrl = async (appInfos?: AppInfos) => { }; const listenEvents = () => ({ - getBundleUrl: (appInfos: AppInfos) => getUpdateFileUrl(appInfos), + getBundleUrl: ev( + 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 0417998ba..21c00ff60 100644 --- a/packages/api/services/auth/index.ts +++ b/packages/api/services/auth/index.ts @@ -18,19 +18,18 @@ import { validate, } from "../collection/user/util"; import namespaces from "../namespaces"; -import { - generateAccessToken, - getHashedPassword, - 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) => { @@ -59,109 +58,116 @@ const listenEvents = () => ({ } }), - 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); + changePassword: ev( + v.object({ + password: v.string(), + password2: v.string(), + token: v.string(), + }), + )( + async ({ password, password2, 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 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); + } 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); + } + }, + ); + }), + ), + + getCsrf: async () => "", + + 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); - } - }, - ); - }), - getCsrf: async () => "", - - 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: { + const privileges = ( + await transaction.userPermission.findMany({ + where: { + username, + }, + }) + ).groupBy("role", "privilege"); + const token = generateAccessToken({ + id: user.id, 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); - } - }); - }), + hashedPassword, + privileges, + }); - login: async ({ - username, - password, - }: { - username: string; - password: string; - }) => { + resolve(token); + } + }); + }), + ), + + login: ev( + v.object({ + username: v.string(), + password: v.string(), + }), + )(async ({ username, password }) => { const hashedPassword = getHashedPassword(password); const user = await prismaDm.user.findFirst({ where: { @@ -176,7 +182,7 @@ const listenEvents = () => ({ } else { return { error: "Invalid username or password" }; } - }, + }), loginAsDemo: async () => { const demoUser = await prismaDm.user.findFirst({ 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 dbbc7ed38..fcdccafe3 100644 --- a/packages/api/services/bookstores/index.ts +++ b/packages/api/services/bookstores/index.ts @@ -1,6 +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, @@ -20,7 +21,6 @@ import { import namespaces from "../namespaces"; import { isAllowedToCreateBookstoreComment } from "./util"; - const persistContribution = async ( user: user, newPoints: number, @@ -51,16 +51,21 @@ const persistContribution = async ( }); }; -const getBookstores = (onlyActive?: true): Promise<(bookstore & { comments: bookstoreComment[] })[]> => +const getBookstores = ( + onlyActive?: true, +): Promise<(bookstore & { comments: bookstoreComment[] })[]> => prismaDm.bookstore.findMany({ - include: onlyActive ? { comments: { - where: { - isActive: true, + include: onlyActive + ? { + comments: { + where: { + isActive: true, + }, + }, + } + : { + comments: true, }, - } - } : { - comments: true, - }, where: onlyActive ? { comments: { @@ -74,7 +79,7 @@ const getBookstores = (onlyActive?: true): Promise<(bookstore & { comments: book const adminListenEvents = () => ({ getBookstores: () => getBookstores(), - approveBookstoreComment: async (commentId: number) => { + approveBookstoreComment: ev(v.number())(async (commentId) => { let bookstoreComment: bookstoreComment; try { bookstoreComment = await prismaDm.bookstoreComment.findUniqueOrThrow({ @@ -105,7 +110,7 @@ const adminListenEvents = () => ({ }); await persistContribution(user, 1, bookstoreComment); } - }, + }), }); export const { client: adminClient, server: adminServer } = useSocketEvents< @@ -119,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 }, }); @@ -128,12 +133,31 @@ 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.pipe( + v.object({ + bookstore: v.union([ + v.object({ + id: v.number(), + }), + v.object({ + name: v.string(), + address: v.string(), + coordX: v.number(), + coordY: v.number(), + }), + ]), + comment: 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: { @@ -143,7 +167,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ : null; let dbBookstore: bookstore & { comments: bookstoreComment[] }; - if ('id' in bookstore) { + if ("id" in bookstore) { try { dbBookstore = await prismaDm.bookstore.findUniqueOrThrow({ include: { @@ -190,7 +214,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ }).send(); return createdComment; - }, + }), }); export type AdminClientEvents = (typeof adminClient)["emitEvents"]; diff --git a/packages/api/services/bookstores/util.ts b/packages/api/services/bookstores/util.ts index 46b95b992..1927d3474 100644 --- a/packages/api/services/bookstores/util.ts +++ b/packages/api/services/bookstores/util.ts @@ -1,12 +1,16 @@ import type { SimpleBookstore } from "~dm-types/SimpleBookstore"; -export const isAllowedToCreateBookstoreComment = (userId: number, bookstore: SimpleBookstore) => - ! bookstore.comments - .filter((comment) => comment.userId === userId) - .some((comment) => comment.creationDate && ( - typeof comment.creationDate === "string" - ? new Date(comment.creationDate) - : comment.creationDate - ) > new Date(Date.now() - 1000 * 60 * 60 * 24 * 180 /* 180 days */) +export const isAllowedToCreateBookstoreComment = ( + userId: number, + bookstore: SimpleBookstore, +) => + !bookstore.comments + .filter((comment) => comment.userId === userId) + .some( + (comment) => + comment.creationDate && + (typeof comment.creationDate === "string" + ? new Date(comment.creationDate) + : comment.creationDate) > + new Date(Date.now() - 1000 * 60 * 60 * 24 * 180 /* 180 days */), ); - \ No newline at end of file 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 658ea5e5d..72bf3fbae 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 9e43d11a2..f41785a1d 100644 --- a/packages/api/services/coa/issue-details/index.ts +++ b/packages/api/services/coa/issue-details/index.ts @@ -56,18 +56,21 @@ export default { 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 66895ad72..c45ecb6ee 100644 --- a/packages/api/services/coa/issues/index.ts +++ b/packages/api/services/coa/issues/index.ts @@ -1,17 +1,25 @@ 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.literal("title", "fullyindexed")), + )((issuecodes, withFields) => prismaCoa .augmentIssueArrayWithInducksData( issuecodes.map((issuecode) => ({ issuecode })), - (withFields || []).filter((field) => ["title", "fullyindexed"].includes(field)), + (withFields || []).filter((field) => + ["title", "fullyindexed"].includes(field), + ), ) .then((data) => data.groupBy("issuecode")), + ), - getCoaCountByPublicationcode: (publicationcodes: string[]) => + getCoaCountByPublicationcode: ev(v.array(v.string()))((publicationcodes) => prismaCoa.inducks_issue .groupBy({ _count: { @@ -32,8 +40,9 @@ export default { ]), ), ), + ), - getCoaCountByCountrycode: (countrycodes: string[]) => + getCoaCountByCountrycode: ev(v.array(v.string()))((countrycodes) => prismaCoa.inducks_issue .groupBy({ _count: { @@ -58,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, @@ -91,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) @@ -101,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 c0ed7d72e..a5a820ea8 100644 --- a/packages/api/services/coa/publications/index.ts +++ b/packages/api/services/coa/publications/index.ts @@ -1,28 +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) => - prismaCoa.inducks_publication.findUnique({ - where: { publicationcode }, - select: { languagecode: true }, - }).then((publication) => publication?.languagecode ?? null), + 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..440f2d9dc 100644 --- a/packages/api/services/coa/quotations/index.ts +++ b/packages/api/services/coa/quotations/index.ts @@ -1,6 +1,9 @@ import type { inducks_issuequotation } 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"; + const ISSUE_CODE_REGEX = /[a-z]+\/[-A-Z0-9 ]+/g; export const getShownQuotations = < @@ -34,18 +37,20 @@ 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.array(v.string()))( + async (issuesByIssuecodes) => { + 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), + }; + } + }, + ), }; 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 c028597cc..b7496ca0e 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"; @@ -39,14 +38,17 @@ export default ({ _socket }: UserServices) => ({ .then((issues: T[]) => prismaCoa .augmentIssueArrayWithInducksData( - issues as (T & { issuecode: string })[] + issues as (T & { issuecode: string })[], ) .then((data) => data.filter((issue) => "publicationcode" in issue)) - .then(prismaDm.replaceLabelsWithLabelIds) + .then(prismaDm.replaceLabelsWithLabelIds), ); }, - 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 +56,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 +80,16 @@ export default ({ _socket }: UserServices) => ({ buyerId, }, }); - }, - - addOrChangeIssues: async ({ - issuecodes, - purchaseId, - condition, - labelIds, - }: CollectionUpdateMultipleIssues) => { + }), + + addOrChangeIssues: ev( + v.object({ + issuecodes: v.array(v.string()), + purchaseId: v.number(), + condition: v.union([v.null(), v.enum(issue_condition)]), + labelIds: v.array(v.number()), + }), + )(async ({ issuecodes, purchaseId, condition, labelIds }) => { const user = _socket.data.user; let checkedPurchaseId: number | null = null; @@ -101,20 +108,30 @@ export default ({ _socket }: UserServices) => ({ issuecodes, condition, checkedPurchaseId, - labelIds + labelIds, ); - }, - addOrChangeCopies: async ({ - issuecode, - copies, - }: CollectionUpdateSingleIssue) => { + }), + + addOrChangeCopies: ev( + v.object({ + issuecode: v.string(), + copies: v.array( + v.object({ + id: v.number(), + condition: v.enum(issue_condition), + purchaseId: v.number(), + 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[], - userId + .filter((purchaseId) => !!purchaseId), + userId, ); const output = await addOrChangeCopies( @@ -123,28 +140,26 @@ export default ({ _socket }: UserServices) => ({ copies.map(({ id }) => id), copies.map(({ condition }) => condition), checkedPurchaseIds, - copies.map(({ labelIds }) => labelIds) + copies.map(({ labelIds }) => labelIds), ); 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 ( @@ -152,8 +167,8 @@ const addOrChangeIssues = async ( issuecodes: string[], condition: issue_condition | undefined, purchaseId: number | null | undefined, - _labelIds: number[] | undefined -): Promise => { + _labelIds: number[] | undefined, +) => { const existingIssues = await prismaDm.issue.findMany({ where: { issuecode: { @@ -189,7 +204,7 @@ const addOrChangeIssues = async ( (issuecode) => !existingIssues .map(({ issuecode: existingIssuecode }) => existingIssuecode) - .includes(issuecode) + .includes(issuecode), ) .map((issuecode) => prismaDm.issue.create({ @@ -200,7 +215,7 @@ const addOrChangeIssues = async ( userId, creationDate: new Date(), }, - }) + }), ); await prismaDm.$transaction(insertOperations); // TODO handle labels on multiple issues @@ -235,20 +250,25 @@ const addOrChangeCopies = async ( issueIds: (number | null)[], conditions: (issue_condition | null)[], purchaseIds: (number | null)[], - labelIds: (number[] | undefined)[] + labelIds: (number[] | undefined)[], ): Promise => { - let operations = [], deleteOperations = []; + let operations = [], + deleteOperations = []; const previousIssueIds = await prismaDm.issue.findMany({ where: { userId, issuecode, }, }); - const deletedIssueIds = previousIssueIds.filter(({ id }) => !issueIds.includes(id)); + const deletedIssueIds = previousIssueIds.filter( + ({ id }) => !issueIds.includes(id), + ); if (deletedIssueIds.length) { - deleteOperations = deletedIssueIds.map(({ id }) => prismaDm.issue.delete({ - where: { id }, - })); + deleteOperations = deletedIssueIds.map(({ id }) => + prismaDm.issue.delete({ + where: { id }, + }), + ); await prismaDm.$transaction(deleteOperations); } if (issueIds.length) { @@ -300,7 +320,7 @@ const addOrChangeCopies = async ( labelId, issueId, })) || [], - }) + }), ); await prismaDm.$transaction(newIssueLabelsOperations); } @@ -312,12 +332,10 @@ const addOrChangeCopies = async ( export const resetDemo = async () => { const demo = (await prismaDm.demo.findUnique({ where: { id: 1 } }))!; - if ( - !( - getHoursFromDate(demo.lastReset) < getHoursFromDate(new Date()) || - demo.lastReset.getTime() + 3_600_000 < new Date().getTime() - ) - ) { + if (!( + getHoursFromDate(demo.lastReset) < getHoursFromDate(new Date()) || + demo.lastReset.getTime() + 3_600_000 < new Date().getTime() + )) { return; } @@ -356,7 +374,7 @@ export const resetDemo = async () => { const csvPurchases = parse( readFileSync(`${csvPath}demo_purchases.csv`), - { columns: true } + { columns: true }, ); await prismaDm.purchase.createMany({ data: csvPurchases.map(({ date, description }) => ({ @@ -377,7 +395,7 @@ export const resetDemo = async () => { const deleteUserData = async ( user: user, - issuesOnly = false + issuesOnly = false, ): Promise => { await prismaDm.issue.deleteMany({ where: { userId: user.id } }); diff --git a/packages/api/services/collection/issues/util.ts b/packages/api/services/collection/issues/util.ts index abd59b717..81f0abbcb 100644 --- a/packages/api/services/collection/issues/util.ts +++ b/packages/api/services/collection/issues/util.ts @@ -25,12 +25,12 @@ export const deleteIssues = async (userId: number, issuecodes: string[]) => { export const checkPurchaseIdsBelongToUser = async ( purchaseIds: number[], - userId: number + userId: number, ): Promise<(number | null)[]> => { const checkedPromiseIds: (number | null)[] = []; for (const purchaseId of purchaseIds) { checkedPromiseIds.push( - (await getUserPurchase(purchaseId, userId)) ? purchaseId : null + (await getUserPurchase(purchaseId, userId)) ? purchaseId : null, ); } return checkedPromiseIds; diff --git a/packages/api/services/collection/labels/index.ts b/packages/api/services/collection/labels/index.ts index ee15955d0..2c81e70f8 100644 --- a/packages/api/services/collection/labels/index.ts +++ b/packages/api/services/collection/labels/index.ts @@ -2,14 +2,18 @@ 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: { description_userId: { description, - userId, - } - }}); + userId, + }, + }, + }); export default ({ _socket }: UserServices) => ({ getLabels: () => @@ -20,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, @@ -39,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, @@ -55,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 77d691c33..affaf8805 100644 --- a/packages/api/services/collection/marketplace/index.ts +++ b/packages/api/services/collection/marketplace/index.ts @@ -13,17 +13,20 @@ 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: ev( - v.pipe(v.array(v.number(`Invalid issue ID list, NaN`)), v.nonEmpty("Invalid issue ID list")), + 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: { @@ -60,31 +63,33 @@ export default (services: UserServices) => { }); }), - 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), }; @@ -120,24 +125,25 @@ export const getIssuesForSale = async (buyerId: number) => WHERE user_collection.issuecode = issue.issuecode AND user_collection.ID_Utilisateur = ${buyerId} )` - .then( - (idsForSale) => - prismaDm.issue.findMany({ - select: { - labels: true, - userId: true, - id: true, - issuecode: true, - }, - where: { - id: { - in: idsForSale.map(({ id }) => id), - }, + .then((idsForSale) => + prismaDm.issue.findMany({ + select: { + labels: true, + userId: true, + id: true, + issuecode: true, + }, + where: { + id: { + in: idsForSale.map(({ id }) => id), }, - }), + }, + }), ) .then((issues: T[]) => - prismaCoa.augmentIssueArrayWithInducksData( - issues as (T & { issuecode: string })[], - ).then(prismaDm.replaceLabelsWithLabelIds), + prismaCoa + .augmentIssueArrayWithInducksData( + issues as (T & { issuecode: string })[], + ) + .then(prismaDm.replaceLabelsWithLabelIds), ); 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 484458d58..e3cbe309b 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,7 +24,10 @@ export default ({ _socket }: UserServices) => ({ })), ), - createPurchase: async (date: string, description: string) => { + createPurchase: ev( + v.string(), + v.string(), + )(async (date, description) => { const criteria = { userId: _socket.data.user.id, date: new Date(date), @@ -30,7 +36,7 @@ export default ({ _socket }: UserServices) => ({ if (Number.isNaN(criteria.date.getTime())) { return { error: `Invalid date: ${date}` } as const; } - + if ( (await prismaDm.purchase.count({ where: criteria, @@ -42,9 +48,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 +62,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 4a2b7cb96..6de92d48b 100644 --- a/packages/api/services/collection/subscriptions/index.ts +++ b/packages/api/services/collection/subscriptions/index.ts @@ -4,6 +4,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 type SubscriptionTransformedStringDates = Omit< subscription, "startDate" | "endDate" @@ -30,18 +33,30 @@ export default ({ _socket }: UserServices) => ({ })), ), - createSubscription: async (subscription: EditSubscription) => { + createSubscription: ev( + v.object({ + id: v.null(), + 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({ + id: v.number(), + 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,7 +65,7 @@ export default ({ _socket }: UserServices) => ({ }, }, }); - }, + }), }); export async function upsertSubscription( diff --git a/packages/api/services/collection/user/index.ts b/packages/api/services/collection/user/index.ts index f644b70fc..681346fe6 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,17 @@ export default ({ _socket }: UserServices) => ({ }); }, - updateUser: async (input: UserForAccountForm) => { + updateUser: ev( + v.object({ + userId: v.number(), + discordId: v.string(), + email: v.string(), + allowSharing: v.boolean(), + marketplaceAcceptsExchanges: v.boolean(), + presentationText: v.string(), + password: v.null(), + }), + )(async (input) => { let hasRequestedPresentationSentenceUpdate = false; let validators: Validation[] = [ new DiscordIdValidation(), @@ -117,5 +129,5 @@ export default ({ _socket }: UserServices) => ({ } }); }); - }, + }), }); diff --git a/packages/api/services/collection/watched-authors/index.ts b/packages/api/services/collection/watched-authors/index.ts index d47b3295b..3827d46b7 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.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 338870be8..8e6790d8a 100644 --- a/packages/api/services/cover-id/index.ts +++ b/packages/api/services/cover-id/index.ts @@ -12,19 +12,23 @@ import namespaces from "../namespaces"; import { getPastecStatus } from "../status"; const listenEvents = () => ({ - searchFromCover: ev(v.pipe( - v.string("Invalid URL or base64 string"), - v.nonEmpty("Invalid URL or base64 string"), - ), v.pipe(v.picklist([0, 1], "Invalid pastec index")))(async (urlOrBase64, pastecIndex) => { - const hostAndPort = process.env.PASTEC_HOSTS_AND_PORTS!.split(",")[pastecIndex]; + searchFromCover: ev( + v.pipe( + v.string("Invalid URL or base64 string"), + v.nonEmpty("Invalid URL or base64 string"), + ), + v.pipe(v.picklist([0, 1], "Invalid pastec index")), + )(async (urlOrBase64, pastecIndex) => { + const hostAndPort = + process.env.PASTEC_HOSTS_AND_PORTS!.split(",")[pastecIndex]; console.log(`Searching from cover on ${hostAndPort}`); const buffer = urlOrBase64.includes(";base64,") ? ( - await axios.get(urlOrBase64, { - responseType: "arraybuffer", - }) - ).data - : Buffer.from(urlOrBase64.split(";base64,").pop()!, "base64"); + await axios.get(urlOrBase64, { + responseType: "arraybuffer", + }) + ).data + : Buffer.from(urlOrBase64.split(";base64,").pop() as string, "base64"); const pastecResponse = await getSimilarImages(buffer, hostAndPort); @@ -58,7 +62,7 @@ const listenEvents = () => ({ id: coverIdByIssuecode[issuecode], score: pastecResponse.scores[ - pastecResponse.image_ids.indexOf(coverIdByIssuecode[issuecode]) + pastecResponse.image_ids.indexOf(coverIdByIssuecode[issuecode]) ], })), ), @@ -67,7 +71,7 @@ const listenEvents = () => ({ covers.sort((cover1, cover2) => Math.sign( pastecResponse.image_ids.indexOf(cover1.id) - - pastecResponse.image_ids.indexOf(cover2.id), + pastecResponse.image_ids.indexOf(cover2.id), ), ), ); @@ -83,47 +87,52 @@ const listenEvents = () => ({ issuecode, fullUrl, score, - boundingRect: pastecResponse.bounding_rects[ - pastecResponse.image_ids.indexOf( - coversByIssuecode.find((cover) => cover.issuecode === issuecode)!.id, - ) - ], + boundingRect: + pastecResponse.bounding_rects[ + pastecResponse.image_ids.indexOf( + coversByIssuecode.find((cover) => cover.issuecode === issuecode)! + .id, + ) + ], })), }; }), 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( @@ -154,27 +163,25 @@ const getCoverUrl = async (coverId: number) => }) .then( (cover) => - `${cover.sitecode}/${cover.sitecode === "webusers" ? "webusers" : ""}${cover.url + `${cover.sitecode}/${cover.sitecode === "webusers" ? "webusers" : ""}${ + cover.url }`, ); -const getSimilarImages = async ( - cover: Buffer, - hostAndPort: string, -) => +const getSimilarImages = async (cover: Buffer, hostAndPort: string) => !process.env.PASTEC_HOSTS_AND_PORTS!.split(",").includes(hostAndPort) ? null : axios - .post( - `http://${hostAndPort}/index/searcher`, - cover, - { - headers: { - "Content-Type": "application/octet-stream", + .post( + `http://${hostAndPort}/index/searcher`, + cover, + { + headers: { + "Content-Type": "application/octet-stream", + }, }, - }, - ) - .then(({ data }) => data) - .catch((e) => { - console.error(e); - }); + ) + .then(({ data }) => data) + .catch((e) => { + console.error(e); + }); 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 b50a59bcb..dab4e58bf 100644 --- a/packages/api/services/edgecreator/multiple-edge-photos/index.ts +++ b/packages/api/services/edgecreator/multiple-edge-photos/index.ts @@ -42,20 +42,25 @@ export default ({ _socket }: UserServices) => ({ 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 f64287cbe..de901a493 100644 --- a/packages/api/services/edges/index.ts +++ b/packages/api/services/edges/index.ts @@ -82,7 +82,8 @@ const listenEvents = () => ({ )((filters) => getEdges(filters) .then((edges) => prismaCoa.augmentIssueArrayWithInducksData(edges)) - .then((edges) => edges.groupBy("issuecode"))), + .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 07bc10d6c..194ea4e63 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"; @@ -106,7 +109,7 @@ const listenEvents = () => ({ ...user, name: "name" in user ? user.name : user.username, })) - .sort((a, b) => a.name.localeCompare(b.name)) + .sort((a, b) => a.name.localeCompare(b.name)), ), getUserCount: () => prismaDm.user.count(), @@ -119,14 +122,17 @@ 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.array(v.number()))(async (userIds) => + userIds.length + ? { + points: await getMedalPoints(userIds), + stats: await getUsersQuickStats(userIds), + } + : { + error: "Bad request", + errorDetails: "Empty user IDs list", + }, + ), }); export const { client, server } = useSocketEvents( @@ -134,7 +140,7 @@ export const { client, server } = useSocketEvents( { listenEvents, middlewares: [], - } + }, ); export type ClientEvents = (typeof client)["emitEvents"]; diff --git a/packages/api/services/presentation-text/index.ts b/packages/api/services/presentation-text/index.ts index 0ee5ab500..00243b741 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,13 @@ import namespaces from "../namespaces"; export type Decision = "approve" | "refuse"; const listenEvents = () => ({ - approveOrDenyPresentationText: async ( - sentence: string, - userId: number, - decision: Decision, - ) => { + approveOrDenyPresentationText: ev( + v.tuple([ + v.string(), + v.number(), + v.union([v.literal("approve"), v.literal("refuse")]), + ]), + )(async ([sentence, userId, decision]) => { switch (decision) { case "approve": { @@ -36,7 +40,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 068975d44..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({ @@ -20,22 +22,28 @@ const listenEvents = () => ({ return { error: "This user does not allow sharing" }; } return { - issues: await prismaDm.issue.findMany({ - where: { - userId: user.id, - issuecode: { - not: null, + issues: await prismaDm.issue + .findMany({ + where: { + userId: user.id, + issuecode: { + not: null, + }, }, - }, - }).then((issues) => - prismaCoa.augmentIssueArrayWithInducksData( - issues as (issue & { issuecode: string })[], - )).then((issues) => issues.map((issue) => ({ - ...issue, - labelIds: [] as number[], - }))) + }) + .then((issues) => + prismaCoa.augmentIssueArrayWithInducksData( + issues as (issue & { issuecode: string })[], + ), + ) + .then((issues) => + issues.map((issue) => ({ + ...issue, + labelIds: [] as number[], + })), + ), }; - }, + }), }); export const { client, server } = useSocketEvents( diff --git a/packages/api/services/stats/suggestions.ts b/packages/api/services/stats/suggestions.ts index 04d4cb30e..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 = { @@ -63,7 +66,8 @@ type MissingPublications = { }; interface Suggestion - extends PrismaDmStats.missingIssueForUserGetPayload, + extends + PrismaDmStats.missingIssueForUserGetPayload, PrismaDmStats.suggestedIssueForUserGetPayload {} const getStoryDetails = async ( diff --git a/packages/api/services/story-search/index.ts b/packages/api/services/story-search/index.ts index 6b4af2381..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"; @@ -44,46 +46,43 @@ const preprocessImage = async (input: string | Buffer) => { } else { imageBuffer = input; } - + if (!imageBuffer || imageBuffer.length === 0) { throw new Error("Empty image buffer"); } - + console.log("Image buffer stored"); // Validate and process image with error handling // For Sharp 0.34 compatibility, get buffer and metadata separately let data: Buffer; let info: { width: number; height: number; channels: number }; - + try { // Process image: resize to exact size // Using simple resize(width, height) which stretches to exact dimensions // This avoids potential issues with options objects in different sharp versions - const buffer = await sharp(imageBuffer) - .resize(224, 224) - .raw() - .toBuffer(); - + const buffer = await sharp(imageBuffer).resize(224, 224).raw().toBuffer(); + // For raw buffers, calculate dimensions from buffer size // Buffer size = width * height * channels // We know width=224, height=224, so channels = buffer.length / (224 * 224) const expectedSize = 224 * 224; const channels = Math.floor(buffer.length / expectedSize); - + if (channels < 3 || channels > 4) { throw new Error( - `Unexpected buffer size: ${buffer.length} bytes. Expected ${expectedSize * 3} (RGB) or ${expectedSize * 4} (RGBA), got ${expectedSize * channels}` + `Unexpected buffer size: ${buffer.length} bytes. Expected ${expectedSize * 3} (RGB) or ${expectedSize * 4} (RGBA), got ${expectedSize * channels}`, ); } - + // Extract info from calculated values (not metadata, which may return original dimensions) info = { width: 224, height: 224, channels: channels, }; - + data = buffer; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -93,14 +92,14 @@ const preprocessImage = async (input: string | Buffer) => { { cause: error }, ); } - + // Validate image dimensions if (info.width !== 224 || info.height !== 224) { throw new Error( - `Invalid image dimensions: expected 224x224, got ${info.width}x${info.height}` + `Invalid image dimensions: expected 224x224, got ${info.width}x${info.height}`, ); } - + // Handle RGBA (4 channels) by converting to RGB let rgbData: Uint8Array; if (info.channels === 4) { @@ -116,13 +115,13 @@ const preprocessImage = async (input: string | Buffer) => { rgbData = data; } else { throw new Error( - `Unsupported number of channels: expected 3 or 4, got ${info.channels}` + `Unsupported number of channels: expected 3 or 4, got ${info.channels}`, ); } - + if (rgbData.length !== 224 * 224 * 3) { throw new Error( - `Invalid image data length: expected ${224 * 224 * 3}, got ${rgbData.length}` + `Invalid image data length: expected ${224 * 224 * 3}, got ${rgbData.length}`, ); } @@ -131,7 +130,7 @@ const preprocessImage = async (input: string | Buffer) => { // ImageNet normalization for EfficientNet: normalize to [0,1] then apply mean/std const mean = [0.485, 0.456, 0.406]; const std = [0.229, 0.224, 0.225]; - + for (let i = 0; i < rgbData.length; i++) { const channel = i % 3; // R=0, G=1, B=2 float32Data[i] = (rgbData[i] / 255 - mean[channel]) / std[channel]; @@ -150,7 +149,7 @@ const preprocessImage = async (input: string | Buffer) => { // Validate tensor before returning if (transposed.length !== 3 * 224 * 224) { throw new Error( - `Invalid tensor size: expected ${3 * 224 * 224}, got ${transposed.length}` + `Invalid tensor size: expected ${3 * 224 * 224}, got ${transposed.length}`, ); } @@ -159,34 +158,34 @@ const preprocessImage = async (input: string | Buffer) => { const getEmbedding = async (input: string | Buffer) => { const inputTensor = await preprocessImage(input); - + // Validate session is available const session = await getSession(); if (!session) { throw new Error("ONNX session not initialized"); } - + // Validate tensor if (!inputTensor || !inputTensor.data) { throw new Error("Invalid input tensor"); } - + try { // Run inference with explicit input name matching the model export const output = await session.run({ input: inputTensor }); - + // Validate output if (!output || !output.embedding) { throw new Error("Model output is missing 'embedding' field"); } - + const embeddingData = output.embedding.data; if (!embeddingData || !(embeddingData instanceof Float32Array)) { throw new Error( - `Invalid embedding data type: expected Float32Array, got ${typeof embeddingData}` + `Invalid embedding data type: expected Float32Array, got ${typeof embeddingData}`, ); } - + return embeddingData; // normalized embedding } catch (error) { // Provide more context about the error @@ -262,10 +261,12 @@ export const findSimilarImages = async ( LIMIT 5 `; console.log("Query done, results:", results); - return { results: results.map(({ sitecode, url, ...rest }) => ({ - ...rest, - fullUrl: getPrefixedEntryurl(url, sitecode) - })) } as const; + return { + results: results.map(({ sitecode, url, ...rest }) => ({ + ...rest, + fullUrl: getPrefixedEntryurl(url, sitecode), + })), + } as const; } catch (error) { console.error("Error finding similar images:", error); return { @@ -275,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) + : ({ error: "Session not initialized" } as const), + ), }); export const { client, server } = useSocketEvents< 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; -}; From 559e76ea9343a469be6abb0d81b4f2de5b590d20 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 16 Sep 2026 21:33:40 +0200 Subject: [PATCH 03/17] Add runtime input validation --- apps/duckguessr/api/services/maintenance.ts | 12 +++---- packages/api/services/auth/index.ts | 30 +++++++++-------- packages/api/services/coa/quotations/index.ts | 30 ++++++++--------- .../services/collection/purchases/index.ts | 11 ++++--- .../api/services/collection/user/index.ts | 32 ++++++++++++------- packages/api/services/collection/user/util.ts | 2 +- packages/api/services/global-stats/index.ts | 17 ++++------ 7 files changed, 70 insertions(+), 64 deletions(-) diff --git a/apps/duckguessr/api/services/maintenance.ts b/apps/duckguessr/api/services/maintenance.ts index 1657635bc..a3fd15508 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -26,13 +26,13 @@ const listenEvents = () => ({ `, getMaintenanceDataForDataset: ev( - v.pipe(v.string()), - v.pipe(v.array(v.enum({ ...entryurlDetailsDecision, null: "null" }))), - v.pipe(v.number()), + v.string(), + v.pipe( + v.array(v.enum({ ...entryurlDetailsDecision, null: "null" })), + v.nonEmpty("No decisions provided" as const), + ), + v.number(), )(async (datasetName, decisions, offset) => { - if (!decisions) { - throw new Error("No decisions provided"); - } const dataset = await prisma.dataset.findUnique({ where: { name: datasetName, diff --git a/packages/api/services/auth/index.ts b/packages/api/services/auth/index.ts index 21c00ff60..600bd5599 100644 --- a/packages/api/services/auth/index.ts +++ b/packages/api/services/auth/index.ts @@ -59,13 +59,23 @@ const listenEvents = () => ({ }), changePassword: ev( - v.object({ - password: v.string(), - password2: v.string(), - token: v.string(), - }), + 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, password2, token }) => + async ({ password, token }) => new Promise>((resolve) => { jwt.verify( token, @@ -73,14 +83,6 @@ const listenEvents = () => ({ 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); } else { const hashedPassword = crypto .createHash("sha1") diff --git a/packages/api/services/coa/quotations/index.ts b/packages/api/services/coa/quotations/index.ts index 440f2d9dc..3b9051c2a 100644 --- a/packages/api/services/coa/quotations/index.ts +++ b/packages/api/services/coa/quotations/index.ts @@ -4,7 +4,7 @@ import { prismaClient as prismaCoa } from "~prisma-schemas/schemas/coa/client"; import { ev } from "socket-call-server/valibot"; import * as v from "valibot"; -const ISSUE_CODE_REGEX = /[a-z]+\/[-A-Z0-9 ]+/g; +const ISSUE_CODE_REGEX = /[a-z]+\/[-A-Z0-9 ]+/; export const getShownQuotations = < Quotation extends Pick< @@ -37,20 +37,16 @@ export const getQuotationsByIssuecodes = async (issuecodes: string[]) => .then(getShownQuotations); export default { - getQuotationsByIssuecodes: ev(v.array(v.string()))( - async (issuesByIssuecodes) => { - 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/purchases/index.ts b/packages/api/services/collection/purchases/index.ts index e3cbe309b..83b5b8db2 100644 --- a/packages/api/services/collection/purchases/index.ts +++ b/packages/api/services/collection/purchases/index.ts @@ -25,7 +25,13 @@ export default ({ _socket }: UserServices) => ({ ), createPurchase: ev( - v.string(), + v.pipe( + v.string(), + v.check( + (date) => !Number.isNaN(new Date(date).getTime()), + "Invalid date" as const, + ), + ), v.string(), )(async (date, description) => { const criteria = { @@ -33,9 +39,6 @@ export default ({ _socket }: UserServices) => ({ date: new Date(date), description, }; - if (Number.isNaN(criteria.date.getTime())) { - return { error: `Invalid date: ${date}` } as const; - } if ( (await prismaDm.purchase.count({ diff --git a/packages/api/services/collection/user/index.ts b/packages/api/services/collection/user/index.ts index 681346fe6..d50d0c5e3 100644 --- a/packages/api/services/collection/user/index.ts +++ b/packages/api/services/collection/user/index.ts @@ -45,15 +45,22 @@ export default ({ _socket }: UserServices) => ({ }, updateUser: ev( - v.object({ - userId: v.number(), - discordId: v.string(), - email: v.string(), - allowSharing: v.boolean(), - marketplaceAcceptsExchanges: v.boolean(), - presentationText: v.string(), - password: v.null(), - }), + 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[] = [ @@ -62,7 +69,6 @@ export default ({ _socket }: UserServices) => ({ new EmailUpdateValidation(), new PresentationTextValidation(), ]; - input.userId = _socket.data.user.id; if (input.password) { validators = [ ...validators, @@ -80,7 +86,11 @@ export default ({ _socket }: UserServices) => ({ let hasResolved = false; prismaDm .$transaction(async (transaction) => { - const scopedError = await validate(transaction, input, validators); + const scopedError = await validate( + transaction, + { ...input, userId: _socket.data.user.id }, + validators, + ); if (scopedError) { resolve({ error: "Bad request", ...scopedError } as const); hasResolved = true; diff --git a/packages/api/services/collection/user/util.ts b/packages/api/services/collection/user/util.ts index 70eb19b9e..0eea0ed40 100644 --- a/packages/api/services/collection/user/util.ts +++ b/packages/api/services/collection/user/util.ts @@ -126,7 +126,7 @@ export class EmailUpdateValidation extends Validation { export class EmailValidation extends Validation { run = async ({ email }: Pick) => { - if (!v.parse(v.pipe(v.string(), v.email()), 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/global-stats/index.ts b/packages/api/services/global-stats/index.ts index 194ea4e63..bd318dc72 100644 --- a/packages/api/services/global-stats/index.ts +++ b/packages/api/services/global-stats/index.ts @@ -122,17 +122,12 @@ const listenEvents = () => ({ }, }), - getUsersPointsAndStats: ev(v.array(v.number()))(async (userIds) => - 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( From a678ea548a58a2add1876b51d793bcd3cd11de1d Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Mon, 21 Sep 2026 16:59:39 +0200 Subject: [PATCH 04/17] dumili-api: Refactor release date validation logic Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/dumili/api/services/indexation/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/dumili/api/services/indexation/index.ts b/apps/dumili/api/services/indexation/index.ts index f92f31ae5..123bf494c 100644 --- a/apps/dumili/api/services/indexation/index.ts +++ b/apps/dumili/api/services/indexation/index.ts @@ -1151,10 +1151,8 @@ const listenEvents = (services: IndexationServices) => ({ "Invalid number of pages" as const, ), v.check( - (indexation) => - !("releaseDate" in indexation) || - indexation.releaseDate === null || - !!new Date(indexation.releaseDate), + ({ releaseDate }) => + releaseDate === null || !Number.isNaN(new Date(releaseDate).getTime()), "Invalid release date" as const, ), ), From 9447fbaf1b696658f617f0ebfddbd4ee7bd36441 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Mon, 21 Sep 2026 17:06:27 +0200 Subject: [PATCH 05/17] dumili-api: Refactor filters validation in getEdges function Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/api/services/edges/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/services/edges/index.ts b/packages/api/services/edges/index.ts index de901a493..9f3154b50 100644 --- a/packages/api/services/edges/index.ts +++ b/packages/api/services/edges/index.ts @@ -72,12 +72,12 @@ const listenEvents = () => ({ .then((issues) => prismaCoa.augmentIssueArrayWithInducksData(issues)), getEdges: ev( - v.config( + v.pipe( v.object({ publicationcode: v.optional(v.string()), issuecodes: v.optional(v.array(v.string())), }), - { message: "Invalid filters" }, + v.check(({ publicationcode, issuecodes }) => !!publicationcode || !!issuecodes?.length, "Invalid filters" as const), ), )((filters) => getEdges(filters) From 25552ce6ab9bee02e55a5f4da7e6a98e3dbaced7 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Mon, 21 Sep 2026 17:03:10 +0200 Subject: [PATCH 06/17] Add runtime input validation --- apps/edgecreator/api/services/upload/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/edgecreator/api/services/upload/index.ts b/apps/edgecreator/api/services/upload/index.ts index 7eb79a890..ea958f6e5 100644 --- a/apps/edgecreator/api/services/upload/index.ts +++ b/apps/edgecreator/api/services/upload/index.ts @@ -156,15 +156,15 @@ const listenEvents = ({ _socket: socket }: UploadServices) => ({ data: v.string(), issuecode: v.string(), isEdgePhoto: v.literal(true), - fileName: v.undefined(), }), ]), - )(async ({ issuecode, data, isEdgePhoto, fileName }) => { + )(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; From 28c5d1abcca4da3a51f193a33977df0c6525b140 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Mon, 21 Sep 2026 17:05:41 +0200 Subject: [PATCH 07/17] Add runtime input validation --- packages/api/services/collection/subscriptions/index.ts | 1 - packages/types/EditSubscription.ts | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/api/services/collection/subscriptions/index.ts b/packages/api/services/collection/subscriptions/index.ts index 6de92d48b..3c7aa8ad1 100644 --- a/packages/api/services/collection/subscriptions/index.ts +++ b/packages/api/services/collection/subscriptions/index.ts @@ -35,7 +35,6 @@ export default ({ _socket }: UserServices) => ({ createSubscription: ev( v.object({ - id: v.null(), publicationcode: v.string(), startDate: v.string(), endDate: v.string(), diff --git a/packages/types/EditSubscription.ts b/packages/types/EditSubscription.ts index 0f818f094..25fc08bde 100644 --- a/packages/types/EditSubscription.ts +++ b/packages/types/EditSubscription.ts @@ -1,6 +1,9 @@ export type EditSubscription = { - id: number | null; publicationcode: string; startDate: string; endDate: string; -}; +}|{ + id: number; + publicationcode: string; + startDate: string; + endDate: string;}; From 553d6df89a30ef9bdf2bac8d39328b1ef4fde4b9 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Tue, 22 Sep 2026 08:24:34 +0200 Subject: [PATCH 08/17] Add runtime input validation --- apps/dumili/api/services/indexation/index.ts | 6 ++-- apps/edgecreator/api/services/legacy/index.ts | 16 ++++----- apps/web/src/pages/image-search-test.vue | 2 +- apps/web/src/stores/coa.ts | 23 +++++++----- apps/web/src/stores/collection.ts | 4 ++- apps/web/src/stores/marketplace.ts | 7 ++-- apps/web/src/stores/public-collection.ts | 7 ++-- packages/api/services/bookstores/index.ts | 36 +++++++++---------- packages/api/services/coa/issues/index.ts | 2 +- .../api/services/collection/issues/index.ts | 14 ++++---- .../collection/subscriptions/index.ts | 8 +++-- .../collection/watched-authors/index.ts | 2 +- packages/api/services/cover-id/index.ts | 2 +- packages/api/services/edges/index.ts | 6 +++- .../api/services/presentation-text/index.ts | 10 +++--- .../schemas/coa/extended/index.ts | 2 +- packages/types/EditSubscription.ts | 9 ----- 17 files changed, 79 insertions(+), 77 deletions(-) delete mode 100644 packages/types/EditSubscription.ts diff --git a/apps/dumili/api/services/indexation/index.ts b/apps/dumili/api/services/indexation/index.ts index 123bf494c..35ae39bcd 100644 --- a/apps/dumili/api/services/indexation/index.ts +++ b/apps/dumili/api/services/indexation/index.ts @@ -1074,7 +1074,6 @@ const listenEvents = (services: IndexationServices) => ({ createStorySuggestion: ev( v.object({ storycode: v.string(), - title: v.string(), entryId: v.number(), }), )(async (suggestion) => @@ -1152,7 +1151,8 @@ const listenEvents = (services: IndexationServices) => ({ ), v.check( ({ releaseDate }) => - releaseDate === null || !Number.isNaN(new Date(releaseDate).getTime()), + releaseDate === null || + !Number.isNaN(new Date(releaseDate).getTime()), "Invalid release date" as const, ), ), @@ -1270,7 +1270,7 @@ const listenEvents = (services: IndexationServices) => ({ v.number(), v.object({ entirepages: v.optional(v.number()), - title: v.optional(v.string()), + title: v.nullable(v.string()), position: v.optional(v.number()), }), )(async (entryId, data) => { diff --git a/apps/edgecreator/api/services/legacy/index.ts b/apps/edgecreator/api/services/legacy/index.ts index d2ddcdebc..714aab92d 100644 --- a/apps/edgecreator/api/services/legacy/index.ts +++ b/apps/edgecreator/api/services/legacy/index.ts @@ -8,12 +8,10 @@ import * as v from "valibot"; const listenEvents = () => ({ getLegacyDimensions: ev( - v.object({ - publicationcode: v.string(), - issuenumber: v.string(), - }), + v.string(), + v.string(), )( - async ({ publicationcode, issuenumber }) => + async (publicationcode, issuenumber) => prismaEdgeCreator.$queryRaw< { optionName: string; optionValue: string }[] >` @@ -33,12 +31,10 @@ const listenEvents = () => ({ ), getLegacySteps: ev( - v.object({ - publicationcode: v.string(), - issuenumber: v.string(), - }), + v.string(), + v.string(), )( - async ({ publicationcode, issuenumber }) => + async (publicationcode, issuenumber) => prismaEdgeCreator.$queryRaw< { stepNumber: number; 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 5769ce584..2a8e8f551 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< @@ -190,7 +197,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 6f02849fe..a343d2eb2 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 040751594..0f1734eda 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/packages/api/services/bookstores/index.ts b/packages/api/services/bookstores/index.ts index fcdccafe3..b4a8f6b68 100644 --- a/packages/api/services/bookstores/index.ts +++ b/packages/api/services/bookstores/index.ts @@ -136,28 +136,24 @@ const listenEvents = ({ _socket }: UserServices) => ({ }), createBookstoreComment: ev( - v.pipe( + v.union([ v.object({ - bookstore: v.union([ - v.object({ - id: v.number(), - }), - v.object({ - name: v.string(), - address: v.string(), - coordX: v.number(), - coordY: v.number(), - }), - ]), - comment: v.object({ - comment: v.string(), - atmosphereRating: v.number(), - pricesRating: v.number(), - selectionRating: v.number(), - }), + id: v.number(), }), - ), - )(async ({ bookstore, comment }) => { + v.object({ + name: v.string(), + 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: { diff --git a/packages/api/services/coa/issues/index.ts b/packages/api/services/coa/issues/index.ts index c45ecb6ee..4a8c6c5a2 100644 --- a/packages/api/services/coa/issues/index.ts +++ b/packages/api/services/coa/issues/index.ts @@ -7,7 +7,7 @@ import * as v from "valibot"; export default { getIssues: ev( v.array(v.string()), - v.array(v.literal("title", "fullyindexed")), + v.array(v.union([v.literal("title"), v.literal("fullyindexed")])), )((issuecodes, withFields) => prismaCoa .augmentIssueArrayWithInducksData( diff --git a/packages/api/services/collection/issues/index.ts b/packages/api/services/collection/issues/index.ts index b7496ca0e..9f9cf9d76 100644 --- a/packages/api/services/collection/issues/index.ts +++ b/packages/api/services/collection/issues/index.ts @@ -85,9 +85,9 @@ export default ({ _socket }: UserServices) => ({ addOrChangeIssues: ev( v.object({ issuecodes: v.array(v.string()), - purchaseId: v.number(), - condition: v.union([v.null(), v.enum(issue_condition)]), - labelIds: v.array(v.number()), + purchaseId: v.optional(v.nullable(v.number())), + 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; @@ -117,9 +117,9 @@ export default ({ _socket }: UserServices) => ({ issuecode: v.string(), copies: v.array( v.object({ - id: v.number(), - condition: v.enum(issue_condition), - purchaseId: v.number(), + id: v.nullable(v.number()), + condition: v.nullable(v.enum(issue_condition)), + purchaseId: v.nullable(v.number()), labelIds: v.array(v.number()), }), ), @@ -130,7 +130,7 @@ export default ({ _socket }: UserServices) => ({ const checkedPurchaseIds = await checkPurchaseIdsBelongToUser( copies .map(({ purchaseId }) => purchaseId) - .filter((purchaseId) => !!purchaseId), + .filter((purchaseId): purchaseId is number => !!purchaseId), userId, ); diff --git a/packages/api/services/collection/subscriptions/index.ts b/packages/api/services/collection/subscriptions/index.ts index 3c7aa8ad1..7cbf1d72e 100644 --- a/packages/api/services/collection/subscriptions/index.ts +++ b/packages/api/services/collection/subscriptions/index.ts @@ -1,4 +1,3 @@ -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"; @@ -46,7 +45,6 @@ export default ({ _socket }: UserServices) => ({ updateSubscription: ev( v.number(), v.object({ - id: v.number(), publicationcode: v.string(), startDate: v.string(), endDate: v.string(), @@ -69,7 +67,11 @@ 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/watched-authors/index.ts b/packages/api/services/collection/watched-authors/index.ts index 3827d46b7..058cf83c4 100644 --- a/packages/api/services/collection/watched-authors/index.ts +++ b/packages/api/services/collection/watched-authors/index.ts @@ -41,7 +41,7 @@ export default ({ _socket }: UserServices) => ({ updateWatchedAuthor: ev( v.object({ personcode: v.string(), - notation: v.number(), + notation: v.nullable(v.number()), }), )(async (data) => { try { diff --git a/packages/api/services/cover-id/index.ts b/packages/api/services/cover-id/index.ts index 09137a83a..a0e75fa81 100644 --- a/packages/api/services/cover-id/index.ts +++ b/packages/api/services/cover-id/index.ts @@ -19,7 +19,7 @@ const listenEvents = () => ({ v.string("Invalid URL or base64 string"), v.nonEmpty("Invalid URL or base64 string"), ), - v.pipe(v.picklist([0, 1], "Invalid pastec index" as const)), + v.pipe(v.optional(v.picklist([0, 1], "Invalid pastec index" as const), 0)), )(async (urlOrBase64, pastecIndex) => { const hostAndPort = process.env.PASTEC_HOSTS_AND_PORTS!.split(",")[pastecIndex]; diff --git a/packages/api/services/edges/index.ts b/packages/api/services/edges/index.ts index 9f3154b50..813af60fb 100644 --- a/packages/api/services/edges/index.ts +++ b/packages/api/services/edges/index.ts @@ -77,7 +77,11 @@ const listenEvents = () => ({ publicationcode: v.optional(v.string()), issuecodes: v.optional(v.array(v.string())), }), - v.check(({ publicationcode, issuecodes }) => !!publicationcode || !!issuecodes?.length, "Invalid filters" as const), + v.check( + ({ publicationcode, issuecodes }) => + !!publicationcode || !!issuecodes?.length, + "Invalid filters" as const, + ), ), )((filters) => getEdges(filters) diff --git a/packages/api/services/presentation-text/index.ts b/packages/api/services/presentation-text/index.ts index 00243b741..b84f6d511 100644 --- a/packages/api/services/presentation-text/index.ts +++ b/packages/api/services/presentation-text/index.ts @@ -13,12 +13,10 @@ export type Decision = "approve" | "refuse"; const listenEvents = () => ({ approveOrDenyPresentationText: ev( - v.tuple([ - v.string(), - v.number(), - v.union([v.literal("approve"), v.literal("refuse")]), - ]), - )(async ([sentence, userId, decision]) => { + v.string(), + v.number(), + v.union([v.literal("approve"), v.literal("refuse")]), + )(async (sentence, userId, decision) => { switch (decision) { case "approve": { diff --git a/packages/prisma-schemas/schemas/coa/extended/index.ts b/packages/prisma-schemas/schemas/coa/extended/index.ts index 621b79608..3f9fb29ba 100644 --- a/packages/prisma-schemas/schemas/coa/extended/index.ts +++ b/packages/prisma-schemas/schemas/coa/extended/index.ts @@ -14,7 +14,7 @@ export 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 25fc08bde..000000000 --- a/packages/types/EditSubscription.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type EditSubscription = { - publicationcode: string; - startDate: string; - endDate: string; -}|{ - id: number; - publicationcode: string; - startDate: string; - endDate: string;}; From cd4f47d445fbd3bdebe300d4163b54456a207d5c Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Tue, 22 Sep 2026 08:43:56 +0200 Subject: [PATCH 09/17] Add runtime input validation --- apps/dumili/api/services/indexation/index.ts | 2 -- packages/api/services/app/index.ts | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dumili/api/services/indexation/index.ts b/apps/dumili/api/services/indexation/index.ts index 35ae39bcd..7561a7f02 100644 --- a/apps/dumili/api/services/indexation/index.ts +++ b/apps/dumili/api/services/indexation/index.ts @@ -1091,8 +1091,6 @@ const listenEvents = (services: IndexationServices) => ({ createIssueSuggestion: ev( v.object({ - title: v.string(), - description: v.string(), publicationcode: v.string(), issuenumber: v.string(), }), diff --git a/packages/api/services/app/index.ts b/packages/api/services/app/index.ts index ca015241a..38001c2e7 100644 --- a/packages/api/services/app/index.ts +++ b/packages/api/services/app/index.ts @@ -34,9 +34,11 @@ export const getUpdateFileUrl = async (appInfos?: AppInfos) => { const listenEvents = () => ({ getBundleUrl: ev( - v.object({ - version: v.string(), - }), + v.optional( + v.object({ + version: v.string(), + }), + ), )((appInfos) => getUpdateFileUrl(appInfos)), }); From 66b7d03c72e7d7e0c65165d603a4e7ff493ab763 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 11:17:49 +0200 Subject: [PATCH 10/17] Regenerate lockfiles --- apps/inducksql/pnpm-lock.yaml | 219 ++++++++----------------- packages/prisma-schemas/pnpm-lock.yaml | 4 +- 2 files changed, 74 insertions(+), 149 deletions(-) diff --git a/apps/inducksql/pnpm-lock.yaml b/apps/inducksql/pnpm-lock.yaml index e1b59cad5..b6ce3d891 100644 --- a/apps/inducksql/pnpm-lock.yaml +++ b/apps/inducksql/pnpm-lock.yaml @@ -95,24 +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==} - - '@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} @@ -193,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] @@ -303,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==} @@ -884,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 @@ -1104,38 +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 - - '@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 @@ -1202,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 @@ -1866,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: @@ -1971,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/packages/prisma-schemas/pnpm-lock.yaml b/packages/prisma-schemas/pnpm-lock.yaml index 34a67820b..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.70.0(typescript@6.0.3) - '@typescript-eslint/types': 8.70.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: From 429ad3e957e19537cbcfcf8ef7c47af3f28d4fe5 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 11:38:54 +0200 Subject: [PATCH 11/17] Add runtime input validation --- apps/duckguessr/api/services/maintenance.ts | 12 +++++------- apps/edgecreator/api/services/image-info/index.ts | 2 +- packages/api/services/cover-id/index.ts | 5 ++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/duckguessr/api/services/maintenance.ts b/apps/duckguessr/api/services/maintenance.ts index a3fd15508..7944fa38c 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -64,13 +64,11 @@ const listenEvents = () => ({ }); }), updateMaintenanceData: ev( - v.pipe( - v.array( - v.object({ - sitecodeUrl: v.string(), - decision: v.enum(entryurlDetailsDecision), - }), - ), + v.array( + v.object({ + sitecodeUrl: v.string(), + decision: v.enum(entryurlDetailsDecision), + }), ), )(async (data) => prisma.$transaction( diff --git a/apps/edgecreator/api/services/image-info/index.ts b/apps/edgecreator/api/services/image-info/index.ts index 6eac16329..fd4217120 100644 --- a/apps/edgecreator/api/services/image-info/index.ts +++ b/apps/edgecreator/api/services/image-info/index.ts @@ -6,7 +6,7 @@ import { ev } from "socket-call-server/valibot"; import * as v from "valibot"; const listenEvents = () => ({ - getImageInfo: ev(v.pipe(v.string()))(async (targetUrl) => { + getImageInfo: ev(v.string())(async (targetUrl) => { const url = targetUrl.startsWith("https://res.cloudinary.com") ? targetUrl : `${process.env.EDGES_URL!}/${targetUrl}`; diff --git a/packages/api/services/cover-id/index.ts b/packages/api/services/cover-id/index.ts index a0e75fa81..18b24ad1f 100644 --- a/packages/api/services/cover-id/index.ts +++ b/packages/api/services/cover-id/index.ts @@ -19,7 +19,10 @@ const listenEvents = () => ({ v.string("Invalid URL or base64 string"), v.nonEmpty("Invalid URL or base64 string"), ), - v.pipe(v.optional(v.picklist([0, 1], "Invalid pastec index" as const), 0)), + 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]; From 9383b2358c534ab47b603480b0fcac44da7a6919 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 13:50:22 +0200 Subject: [PATCH 12/17] Add runtime input validation --- apps/duckguessr/api/services/datasets.ts | 6 +++--- apps/duckguessr/api/services/maintenance.ts | 8 ++++---- apps/duckguessr/app/pages/admin/create-dataset.vue | 4 ++-- apps/dumili/api/services/indexation/index.ts | 5 ++--- apps/dumili/src/pages/index.vue | 2 +- packages/api/services/collection/issues/index.ts | 13 +++++++++++-- packages/api/services/collection/user/index.ts | 6 +++++- 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/duckguessr/api/services/datasets.ts b/apps/duckguessr/api/services/datasets.ts index 16dcdf75f..f8fa48d9c 100644 --- a/apps/duckguessr/api/services/datasets.ts +++ b/apps/duckguessr/api/services/datasets.ts @@ -38,9 +38,9 @@ const listenEvents = () => ({ previewDataset: ev( v.pipe( v.object({ - personNationalityFilter: v.union([v.array(v.string()), v.undefined()]), - oldestDateFilterMin: v.union([v.number(), v.undefined()]), - oldestDateFilterMax: v.union([v.number(), v.undefined()]), + personNationalityFilter: v.optional(v.array(v.string())), + oldestDateFilterMin: v.optional(v.number()), + oldestDateFilterMax: v.optional(v.number()), }), v.check( ({ personNationalityFilter }) => diff --git a/apps/duckguessr/api/services/maintenance.ts b/apps/duckguessr/api/services/maintenance.ts index 7944fa38c..5dad02f04 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -27,12 +27,12 @@ const listenEvents = () => ({ getMaintenanceDataForDataset: ev( v.string(), - v.pipe( - v.array(v.enum({ ...entryurlDetailsDecision, null: "null" })), - v.nonEmpty("No decisions provided" as const), - ), + v.array(v.enum({ ...entryurlDetailsDecision, null: "null" })), v.number(), )(async (datasetName, decisions, offset) => { + if (!decisions.length) { + return []; + } const dataset = await prisma.dataset.findUnique({ where: { name: datasetName, diff --git a/apps/duckguessr/app/pages/admin/create-dataset.vue b/apps/duckguessr/app/pages/admin/create-dataset.vue index 7706b7008..bdf8fcd34 100644 --- a/apps/duckguessr/app/pages/admin/create-dataset.vue +++ b/apps/duckguessr/app/pages/admin/create-dataset.vue @@ -116,7 +116,7 @@ ({ ), v.check( ({ releaseDate }) => - releaseDate === null || - !Number.isNaN(new Date(releaseDate).getTime()), + !releaseDate || !Number.isNaN(new Date(releaseDate).getTime()), "Invalid release date" as const, ), ), @@ -1268,7 +1267,7 @@ const listenEvents = (services: IndexationServices) => ({ v.number(), v.object({ entirepages: v.optional(v.number()), - title: v.nullable(v.string()), + title: v.nullish(v.string()), position: v.optional(v.number()), }), )(async (entryId, data) => { 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 @@ ) }} null), + ), +]); + export default ({ _socket }: UserServices) => ({ getIssues: async () => { if (_socket.data.user.username === "demo") { @@ -85,7 +94,7 @@ export default ({ _socket }: UserServices) => ({ addOrChangeIssues: ev( v.object({ issuecodes: v.array(v.string()), - purchaseId: v.optional(v.nullable(v.number())), + purchaseId: v.optional(purchaseIdSchema), condition: v.optional(v.union([v.null(), v.enum(issue_condition)])), labelIds: v.optional(v.array(v.number())), }), @@ -119,7 +128,7 @@ export default ({ _socket }: UserServices) => ({ v.object({ id: v.nullable(v.number()), condition: v.nullable(v.enum(issue_condition)), - purchaseId: v.nullable(v.number()), + purchaseId: purchaseIdSchema, labelIds: v.array(v.number()), }), ), diff --git a/packages/api/services/collection/user/index.ts b/packages/api/services/collection/user/index.ts index d50d0c5e3..fe10ca14c 100644 --- a/packages/api/services/collection/user/index.ts +++ b/packages/api/services/collection/user/index.ts @@ -88,7 +88,11 @@ export default ({ _socket }: UserServices) => ({ .$transaction(async (transaction) => { const scopedError = await validate( transaction, - { ...input, userId: _socket.data.user.id }, + { + ...input, + id: _socket.data.user.id, + userId: _socket.data.user.id, + }, validators, ); if (scopedError) { From fd961e7eff3eca7ae1145618e1643225c768bc82 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 13:59:03 +0200 Subject: [PATCH 13/17] Add runtime input validation --- apps/duckguessr/app/components/MedalList.vue | 106 +++++++++--------- .../app/pages/admin/create-dataset.vue | 11 +- apps/duckguessr/app/stores/player.ts | 5 +- packages/api/services/bookstores/index.ts | 2 +- .../api/services/collection/issues/index.ts | 1 - 5 files changed, 61 insertions(+), 64 deletions(-) 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 bdf8fcd34..dc3fe1687 100644 --- a/apps/duckguessr/app/pages/admin/create-dataset.vue +++ b/apps/duckguessr/app/pages/admin/create-dataset.vue @@ -237,12 +237,9 @@ Preview Matches -
+
>(); +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, @@ -490,7 +489,7 @@ watch( datasetPreview.value = result; }) .catch((error) => { - console.error("Error previewing dataset:", 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/packages/api/services/bookstores/index.ts b/packages/api/services/bookstores/index.ts index b4a8f6b68..6a6039e3b 100644 --- a/packages/api/services/bookstores/index.ts +++ b/packages/api/services/bookstores/index.ts @@ -141,7 +141,7 @@ const listenEvents = ({ _socket }: UserServices) => ({ id: v.number(), }), v.object({ - name: v.string(), + name: v.pipe(v.string(), v.nonEmpty()), address: v.string(), coordX: v.number(), coordY: v.number(), diff --git a/packages/api/services/collection/issues/index.ts b/packages/api/services/collection/issues/index.ts index f51542acb..b68afec41 100644 --- a/packages/api/services/collection/issues/index.ts +++ b/packages/api/services/collection/issues/index.ts @@ -16,7 +16,6 @@ import type { UserServices } from "../../../index"; import { getShownQuotations } from "../../coa/quotations"; import { checkPurchaseIdsBelongToUser, deleteIssues } from "./util"; -// WhatTheDuck's "no purchase date" radio option carries '' because a null value can't be selected const purchaseIdSchema = v.union([ v.nullable(v.number()), v.pipe( From 06a647fd8209e7c0e3ef8e391111dc8d7d20c001 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 14:22:34 +0200 Subject: [PATCH 14/17] duckguessr-api,dumili-api,edgecreator-api,api: Add some validation checks --- apps/duckguessr/api/get-player.ts | 5 ++- apps/duckguessr/api/services/player.ts | 11 +---- apps/dumili/api/eslint.config.mjs | 2 +- .../api/services/indexation-creation/index.ts | 11 ++++- apps/edgecreator/api/services/upload/index.ts | 8 +++- packages/api/services/coa/countries/index.ts | 40 +++++++++---------- .../collection/subscriptions/index.ts | 3 ++ 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/apps/duckguessr/api/get-player.ts b/apps/duckguessr/api/get-player.ts index 722c3e1f5..a175768ca 100644 --- a/apps/duckguessr/api/get-player.ts +++ b/apps/duckguessr/api/get-player.ts @@ -69,7 +69,10 @@ export const getPlayer = async (cookies?: { return player!; }; -export const updatePlayer = (playerId: number, player: player) => +export const updatePlayer = ( + playerId: number, + player: Pick, +) => prisma.player.update({ where: { id: playerId }, data: player, diff --git a/apps/duckguessr/api/services/player.ts b/apps/duckguessr/api/services/player.ts index 3026fb83c..e4f9789fb 100644 --- a/apps/duckguessr/api/services/player.ts +++ b/apps/duckguessr/api/services/player.ts @@ -27,18 +27,11 @@ export type PlayerServices = NamespaceProxyTarget< Record >; -const playerValidation = v.object({ - id: v.number(), - username: v.string(), - ducksmanagerId: v.nullable(v.number()), - avatar: v.string(), -}); - const listenEvents = ({ _socket }: PlayerServices) => ({ getPlayer: () => Promise.resolve(_socket.data.user), - updateUser: ev(playerValidation)(async (updatedPlayer) => - updatePlayer(updatedPlayer.id, updatedPlayer), + updateUser: ev(v.object({ avatar: v.string() }))(async ({ avatar }) => + updatePlayer(_socket.data.user.id, { avatar }), ), getStats: ev(v.optional(v.number()))(async (gameId) => { diff --git a/apps/dumili/api/eslint.config.mjs b/apps/dumili/api/eslint.config.mjs index 2cfd6bccf..44ae7cb8b 100644 --- a/apps/dumili/api/eslint.config.mjs +++ b/apps/dumili/api/eslint.config.mjs @@ -16,7 +16,7 @@ export default defineConfig( "**/dist", "**/bundle.mjs", "**/*.d.ts", - "prisma/client_dumili/client", + "prisma/client_dumili", "paddleocr", "eslint.config.mjs", ], diff --git a/apps/dumili/api/services/indexation-creation/index.ts b/apps/dumili/api/services/indexation-creation/index.ts index fa09fcfeb..e0326beb3 100644 --- a/apps/dumili/api/services/indexation-creation/index.ts +++ b/apps/dumili/api/services/indexation-creation/index.ts @@ -19,7 +19,16 @@ type IndexationCreationServices = NamespaceProxyTarget< >; const listenEvents = ({ _socket }: IndexationCreationServices) => ({ - create: ev(v.number())(async (numberOfPages) => + 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: { diff --git a/apps/edgecreator/api/services/upload/index.ts b/apps/edgecreator/api/services/upload/index.ts index ea958f6e5..91c03a2d8 100644 --- a/apps/edgecreator/api/services/upload/index.ts +++ b/apps/edgecreator/api/services/upload/index.ts @@ -150,7 +150,13 @@ const listenEvents = ({ _socket: socket }: UploadServices) => ({ data: v.string(), issuecode: v.string(), isEdgePhoto: v.literal(false), - fileName: v.string(), + fileName: v.pipe( + v.string(), + v.check( + (fileName) => !/[/\\]/.test(fileName) && !fileName.includes(".."), + "Invalid file name" as const, + ), + ), }), v.object({ data: v.string(), diff --git a/packages/api/services/coa/countries/index.ts b/packages/api/services/coa/countries/index.ts index 72bf3fbae..4c653915c 100644 --- a/packages/api/services/coa/countries/index.ts +++ b/packages/api/services/coa/countries/index.ts @@ -15,32 +15,28 @@ const getCountryNames = async ( locale: string, countryIds?: string[], ): Promise> => - prismaCoa - .$queryRawUnsafe< - { - countrycode: string; - countryname: string; - default_countryname: string; - }[] - >( - ` + prismaCoa.$queryRaw< + { + countrycode: string; + countryname: string; + default_countryname: string; + }[] + >` SELECT inducks_country.countrycode, inducks_countryname.countryname, inducks_country.countryname AS default_countryname FROM inducks_country LEFT JOIN inducks_countryname on inducks_country.countrycode = inducks_countryname.countrycode - WHERE languagecode = '${locale}' + WHERE languagecode = ${locale} AND ${ countryIds?.length - ? `inducks_country.countrycode IN (${Prisma.join(countryIds)})` - : `inducks_country.countrycode != 'zz'` - }`, - ) - .then((results) => - Object.fromEntries( - results.map((value) => [ - value.countrycode, - value.countryname || value.default_countryname, - ]), - ), - ); + ? Prisma.sql`inducks_country.countrycode IN (${Prisma.join(countryIds)})` + : Prisma.sql`inducks_country.countrycode != 'zz'` + }`.then((results) => + Object.fromEntries( + results.map((value) => [ + value.countrycode, + value.countryname || value.default_countryname, + ]), + ), + ); diff --git a/packages/api/services/collection/subscriptions/index.ts b/packages/api/services/collection/subscriptions/index.ts index 7cbf1d72e..c70e032ac 100644 --- a/packages/api/services/collection/subscriptions/index.ts +++ b/packages/api/services/collection/subscriptions/index.ts @@ -96,6 +96,9 @@ export async function upsertSubscription( startDate: new Date(subscription.startDate), endDate: new Date(subscription.endDate), }; + if (Object.values(dates).some((date) => Number.isNaN(date.getTime()))) { + return null; + } await prismaDm.subscription.upsert({ update: dates, create: { From 9301476946277a305e3698633181a72379a8ad30 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 14:41:41 +0200 Subject: [PATCH 15/17] Add runtime input validation --- .../edgecreator/src/composables/useLegacyDb.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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( From 6f4edf1a7f1136e9ad3ea16b10b9f4f1911e9b49 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 15:15:13 +0200 Subject: [PATCH 16/17] Add runtime input validation --- packages/api/services/coa/issue-details/index.ts | 4 +--- .../api/services/edgecreator/multiple-edge-photos/index.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/api/services/coa/issue-details/index.ts b/packages/api/services/coa/issue-details/index.ts index f41785a1d..89d9a2974 100644 --- a/packages/api/services/coa/issue-details/index.ts +++ b/packages/api/services/coa/issue-details/index.ts @@ -36,9 +36,7 @@ export const getPopularityByIssuecodes = async (issuecodes: string[]) => export default { getIssueDetails: ev( - v.config(v.pipe(v.string(), v.nonEmpty()), { - message: "Invalid issuecode", - }), + v.pipe(v.string("Invalid issuecode"), v.nonEmpty("Invalid issuecode")), )(async (issuecode) => { const issue = await prismaCoa.inducks_issue.findFirst({ where: { issuecode }, diff --git a/packages/api/services/edgecreator/multiple-edge-photos/index.ts b/packages/api/services/edgecreator/multiple-edge-photos/index.ts index dab4e58bf..b07c99e66 100644 --- a/packages/api/services/edgecreator/multiple-edge-photos/index.ts +++ b/packages/api/services/edgecreator/multiple-edge-photos/index.ts @@ -26,9 +26,7 @@ export const checkTodayLimit = (userId: number) => export default ({ _socket }: UserServices) => ({ sendNewEdgePhotoEmail: ev( - v.config(v.pipe(v.string(), v.nonEmpty()), { - message: "Invalid issuecode", - }), + 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 }, From c30a8dbdf5138eccb559e13ccd107ecbd2f4244e Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Wed, 30 Sep 2026 15:27:10 +0200 Subject: [PATCH 17/17] Remove unused types, don't export types unused elsewhere --- apps/coa-updater/snapshot-history.ts | 2 +- apps/duckguessr/api/services/datasets.ts | 7 ------- apps/duckguessr/api/services/game.ts | 2 +- apps/duckguessr/api/services/home.ts | 8 +------- apps/duckguessr/api/services/maintenance.ts | 7 ------- apps/duckguessr/api/services/match.ts | 2 +- apps/duckguessr/api/services/player.ts | 2 +- apps/duckguessr/api/services/podium.ts | 7 ------- apps/duckguessr/api/types/dataset.ts | 6 ------ apps/duckguessr/api/types/game.ts | 2 +- apps/duckguessr/api/types/matchDetails.ts | 10 ---------- apps/duckguessr/api/types/socketEvents.ts | 1 - apps/edgecreator/api/services/browse/index.ts | 2 +- apps/edgecreator/api/services/legacy/index.ts | 7 ------- apps/edgecreator/api/services/save/index.ts | 2 +- apps/edgecreator/api/services/upload/index.ts | 2 +- apps/edgecreator/src/stores/step.ts | 2 +- apps/edgecreator/src/types/StepOptionBaseProps.ts | 8 -------- apps/edgecreator/src/types/StepsPerIssuenumber.ts | 3 --- apps/inducksql/introspect.ts | 4 ++-- .../src/composables/useFrameSampler.ts | 2 +- .../src/composables/useLiveCoverSearch.ts | 2 +- apps/whattheduck/src/stores/wtdcollection.ts | 15 --------------- packages/api/services/rate-limit.ts | 2 +- packages/api/services/stats/watchedAuthors.ts | 7 ------- .../prisma-schemas/schemas/coa/extended/index.ts | 2 +- 26 files changed, 16 insertions(+), 100 deletions(-) delete mode 100644 apps/duckguessr/api/types/dataset.ts delete mode 100644 apps/duckguessr/api/types/matchDetails.ts delete mode 100644 apps/edgecreator/src/types/StepOptionBaseProps.ts delete mode 100644 apps/edgecreator/src/types/StepsPerIssuenumber.ts diff --git a/apps/coa-updater/snapshot-history.ts b/apps/coa-updater/snapshot-history.ts index bcfddb941..d83261cef 100644 --- a/apps/coa-updater/snapshot-history.ts +++ b/apps/coa-updater/snapshot-history.ts @@ -120,7 +120,7 @@ const copyTableFromLive = async ( return affected; }; -export type SnapshotIntoDatedSchemaOptions = { +type SnapshotIntoDatedSchemaOptions = { liveDatabase: string; snapshotDate: string; skipTables?: readonly string[]; diff --git a/apps/duckguessr/api/services/datasets.ts b/apps/duckguessr/api/services/datasets.ts index f8fa48d9c..a76fd82fd 100644 --- a/apps/duckguessr/api/services/datasets.ts +++ b/apps/duckguessr/api/services/datasets.ts @@ -1,5 +1,3 @@ -import type { Socket } from "socket.io"; -import type { NamespaceProxyTarget } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; import { Prisma } from "~prisma-schemas/client_dm/client"; @@ -10,11 +8,6 @@ import namespaces from "./namespaces"; import { ev } from "socket-call-server/valibot"; import * as v from "valibot"; -export type DatasetsServices = NamespaceProxyTarget< - Socket, - Record ->; - const listenEvents = () => ({ getDatasets: async () => prisma.$queryRaw< { diff --git a/apps/duckguessr/api/services/game.ts b/apps/duckguessr/api/services/game.ts index 3c11e85f3..385f5a938 100644 --- a/apps/duckguessr/api/services/game.ts +++ b/apps/duckguessr/api/services/game.ts @@ -39,7 +39,7 @@ export type ClientListenEvents = { firstRoundWillStartSoon: (firstRoundStartDate: Date) => void; }; -export type GameServices = NamespaceProxyTarget< +type GameServices = NamespaceProxyTarget< Socket, ClientListenEvents >; diff --git a/apps/duckguessr/api/services/home.ts b/apps/duckguessr/api/services/home.ts index 953c4a780..eac880ba9 100644 --- a/apps/duckguessr/api/services/home.ts +++ b/apps/duckguessr/api/services/home.ts @@ -1,16 +1,10 @@ -import { type Socket } from "socket.io"; -import { type NamespaceProxyTarget, useSocketEvents } from "socket-call-server"; +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"; -export type HomeServices = NamespaceProxyTarget< - Socket, - Record ->; - const convertUrlToBase64 = async (url: string): Promise => { try { const response = await fetch(url); diff --git a/apps/duckguessr/api/services/maintenance.ts b/apps/duckguessr/api/services/maintenance.ts index 5dad02f04..75bfa9123 100644 --- a/apps/duckguessr/api/services/maintenance.ts +++ b/apps/duckguessr/api/services/maintenance.ts @@ -1,5 +1,3 @@ -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"; @@ -9,11 +7,6 @@ import { entryurlDetailsDecision } from "../prisma/client_duckguessr/browser"; import namespaces from "./namespaces"; import { RequiredPlayerMiddleware } from "../middlewares/required-player"; -export type MaintenanceServices = NamespaceProxyTarget< - Socket, - Record ->; - const listenEvents = () => ({ getMaintenanceData: async () => prisma.$queryRaw< { name: string; decision: string; count: number }[] diff --git a/apps/duckguessr/api/services/match.ts b/apps/duckguessr/api/services/match.ts index d94cba292..ace091039 100644 --- a/apps/duckguessr/api/services/match.ts +++ b/apps/duckguessr/api/services/match.ts @@ -11,7 +11,7 @@ import { createGameSocket } from "./game"; import { ev } from "socket-call-server/valibot"; import * as v from "valibot"; -export type MatchServices = NamespaceProxyTarget< +type MatchServices = NamespaceProxyTarget< Socket< typeof listenEvents, Record, diff --git a/apps/duckguessr/api/services/player.ts b/apps/duckguessr/api/services/player.ts index e4f9789fb..c7201193c 100644 --- a/apps/duckguessr/api/services/player.ts +++ b/apps/duckguessr/api/services/player.ts @@ -15,7 +15,7 @@ export type ClientListenEvents = { loginFailed: () => void; }; -export type PlayerServices = NamespaceProxyTarget< +type PlayerServices = NamespaceProxyTarget< Socket< typeof listenEvents, ClientListenEvents, diff --git a/apps/duckguessr/api/services/podium.ts b/apps/duckguessr/api/services/podium.ts index fce6d48fb..067ecc3d2 100644 --- a/apps/duckguessr/api/services/podium.ts +++ b/apps/duckguessr/api/services/podium.ts @@ -1,16 +1,9 @@ -import type { Socket } from "socket.io"; -import type { NamespaceProxyTarget } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; import prisma from "../prisma/client"; import { type player } from "../prisma/client_duckguessr/browser"; import namespaces from "./namespaces"; -export type PodiumServices = NamespaceProxyTarget< - Socket, - Record ->; - const listenEvents = () => ({ getPodium: async () => prisma.$queryRaw<(player & { sumScore: number })[]>` SELECT player.*, sum(score + speed_bonus) AS sumScore diff --git a/apps/duckguessr/api/types/dataset.ts b/apps/duckguessr/api/types/dataset.ts deleted file mode 100644 index 1614611c2..000000000 --- a/apps/duckguessr/api/types/dataset.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { dataset } from "../prisma/client_duckguessr/browser"; - -export interface DatasetWithCounts extends dataset { - images: number; - authors: number; -} diff --git a/apps/duckguessr/api/types/game.ts b/apps/duckguessr/api/types/game.ts index b6de6c527..42bdd5214 100644 --- a/apps/duckguessr/api/types/game.ts +++ b/apps/duckguessr/api/types/game.ts @@ -10,7 +10,7 @@ import type { UnfinishedRound, } from "./roundWithScoresAndAuthor"; -export interface GamePlayerWithFullPlayer extends gamePlayer { +interface GamePlayerWithFullPlayer extends gamePlayer { player: player; } diff --git a/apps/duckguessr/api/types/matchDetails.ts b/apps/duckguessr/api/types/matchDetails.ts deleted file mode 100644 index 0ba8b6cec..000000000 --- a/apps/duckguessr/api/types/matchDetails.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { - player, - userMedalPoints, -} from "../prisma/client_duckguessr/browser"; - -export interface MatchDetails { - isBotAvailable: boolean; - players: player[]; - playerStats: userMedalPoints[]; -} diff --git a/apps/duckguessr/api/types/socketEvents.ts b/apps/duckguessr/api/types/socketEvents.ts index fca61739f..0fa7033b8 100644 --- a/apps/duckguessr/api/types/socketEvents.ts +++ b/apps/duckguessr/api/types/socketEvents.ts @@ -5,7 +5,6 @@ export type CurrentGame = NonNullable< Awaited> >; -export type InterServerEvents = Record; export type SocketGameData = { user: player; currentRound: round; diff --git a/apps/edgecreator/api/services/browse/index.ts b/apps/edgecreator/api/services/browse/index.ts index 8a30253ec..bce152038 100644 --- a/apps/edgecreator/api/services/browse/index.ts +++ b/apps/edgecreator/api/services/browse/index.ts @@ -14,7 +14,7 @@ import { prismaClient as prismaDm } from "~prisma-schemas/schemas/dm/client"; import type { SessionData } from "../../index"; import { getEdgesPath } from "../../index"; -export type BrowseServices = NamespaceProxyTarget< +type BrowseServices = NamespaceProxyTarget< Socket, Record >; diff --git a/apps/edgecreator/api/services/legacy/index.ts b/apps/edgecreator/api/services/legacy/index.ts index 714aab92d..d604365ba 100644 --- a/apps/edgecreator/api/services/legacy/index.ts +++ b/apps/edgecreator/api/services/legacy/index.ts @@ -1,6 +1,4 @@ -import type { NamespaceProxyTarget } from "socket-call-server"; import { useSocketEvents } from "socket-call-server"; -import type { Socket } from "socket.io"; import { prismaClient as prismaEdgeCreator } from "~prisma-schemas/schemas/edgecreator/client"; import { ev } from "socket-call-server/valibot"; @@ -77,9 +75,4 @@ export const { client, server } = useSocketEvents< middlewares: [], }); -export type LegacyServices = NamespaceProxyTarget< - Socket, - Record ->; - export type ClientEvents = (typeof client)["emitEvents"]; diff --git a/apps/edgecreator/api/services/save/index.ts b/apps/edgecreator/api/services/save/index.ts index 0aac0686e..cf93d88a6 100644 --- a/apps/edgecreator/api/services/save/index.ts +++ b/apps/edgecreator/api/services/save/index.ts @@ -14,7 +14,7 @@ import type { ExportPaths } from "~types/ExportPaths"; import { getSvgPath } from "../../_utils"; -export type SaveServices = NamespaceProxyTarget< +type SaveServices = NamespaceProxyTarget< Socket, Record >; diff --git a/apps/edgecreator/api/services/upload/index.ts b/apps/edgecreator/api/services/upload/index.ts index 91c03a2d8..a2ed90bd7 100644 --- a/apps/edgecreator/api/services/upload/index.ts +++ b/apps/edgecreator/api/services/upload/index.ts @@ -138,7 +138,7 @@ const getTargetFilePath = async ({ return filePath; }; -export type UploadServices = NamespaceProxyTarget< +type UploadServices = NamespaceProxyTarget< Socket, Record >; diff --git a/apps/edgecreator/src/stores/step.ts b/apps/edgecreator/src/stores/step.ts index f97539169..aba000716 100644 --- a/apps/edgecreator/src/stores/step.ts +++ b/apps/edgecreator/src/stores/step.ts @@ -16,7 +16,7 @@ export type Options = StepOption[]; export type OptionsArray = OptionNameAndValue[]; -export interface Dimensions { +interface Dimensions { issuecode: string; width: number; height: number; diff --git a/apps/edgecreator/src/types/StepOptionBaseProps.ts b/apps/edgecreator/src/types/StepOptionBaseProps.ts deleted file mode 100644 index 4d8eb086d..000000000 --- a/apps/edgecreator/src/types/StepOptionBaseProps.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { OptionValue } from "~/types/OptionValue"; - -export interface BaseProps { - issuecode: string; - stepNumber: number; - - options: Record | undefined; -} diff --git a/apps/edgecreator/src/types/StepsPerIssuenumber.ts b/apps/edgecreator/src/types/StepsPerIssuenumber.ts deleted file mode 100644 index 0b7d5ffbd..000000000 --- a/apps/edgecreator/src/types/StepsPerIssuenumber.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Step } from "~/types/Step"; - -export type StepsPerIssuecode = Record; diff --git a/apps/inducksql/introspect.ts b/apps/inducksql/introspect.ts index 2aed318ec..c467a0272 100644 --- a/apps/inducksql/introspect.ts +++ b/apps/inducksql/introspect.ts @@ -2,7 +2,7 @@ import type { Pool } from "mariadb"; import { excludedTablePrefixes, excludedTables } from "./config"; -export type Column = { +type Column = { name: string; dataType: string; columnType: string; @@ -10,7 +10,7 @@ export type Column = { autoIncrement: boolean; }; -export type Index = { +type Index = { name: string; columns: string[]; unique: boolean; diff --git a/apps/whattheduck/src/composables/useFrameSampler.ts b/apps/whattheduck/src/composables/useFrameSampler.ts index b41bdcf32..73799f499 100644 --- a/apps/whattheduck/src/composables/useFrameSampler.ts +++ b/apps/whattheduck/src/composables/useFrameSampler.ts @@ -46,7 +46,7 @@ export interface FrameSize { height: number; } -export type PreparedFrame = +type PreparedFrame = | { status: 'ready'; dataUrl: string; size: FrameSize; metrics: FrameMetrics } | { status: 'skipped'; reason: 'blurry' | 'unchanged'; metrics: FrameMetrics }; diff --git a/apps/whattheduck/src/composables/useLiveCoverSearch.ts b/apps/whattheduck/src/composables/useLiveCoverSearch.ts index 2f1f8dd93..81bec0344 100644 --- a/apps/whattheduck/src/composables/useLiveCoverSearch.ts +++ b/apps/whattheduck/src/composables/useLiveCoverSearch.ts @@ -27,7 +27,7 @@ export const liveCoverSearchConfig = { }, }; -export type LivePhase = 'idle' | 'scanning' | 'matched' | 'confirming' | 'exhausted'; +type LivePhase = 'idle' | 'scanning' | 'matched' | 'confirming' | 'exhausted'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/whattheduck/src/stores/wtdcollection.ts b/apps/whattheduck/src/stores/wtdcollection.ts index 6f9b8df7a..c8615d5fa 100644 --- a/apps/whattheduck/src/stores/wtdcollection.ts +++ b/apps/whattheduck/src/stores/wtdcollection.ts @@ -1,18 +1,11 @@ import { defineStore } from 'pinia'; -import type { EntryPartInfo } from '~dm-types/EntryPartInfo'; -import type { IssueWithIssuecodeOnly } from '~dm-types/IssueWithIssuecodeOnly'; -import type { issue, purchase } from '~prisma-schemas/schemas/dm'; import useCollection from '~web/src/composables/useCollection'; import { coa } from '~web/src/stores/coa'; import { collection } from '~web/src/stores/collection'; import { stats } from '~web/src/stores/stats'; import { users } from '~web/src/stores/users'; -export type purchaseWithStringDate = Omit & { - date: string; -}; - export const wtdcollection = defineStore('wtdcollection', () => { const coaStore = coa(); const webCollectionStore = collection(); @@ -123,11 +116,3 @@ export const wtdcollection = defineStore('wtdcollection', () => { user, }; }); - -export type IssueWithCollectionIssues = IssueWithIssuecodeOnly & { - countrycode: string; - countryname?: string; - publicationName: string; - collectionIssues?: issue[]; - partInfo?: EntryPartInfo; -}; diff --git a/packages/api/services/rate-limit.ts b/packages/api/services/rate-limit.ts index 00f43a2fa..006bda1c6 100644 --- a/packages/api/services/rate-limit.ts +++ b/packages/api/services/rate-limit.ts @@ -5,7 +5,7 @@ * scaled horizontally this needs to move to a shared store. */ -export type RateLimitResult = +type RateLimitResult = | { allowed: true; remaining: number } | { allowed: false; retryAfterMs: number }; diff --git a/packages/api/services/stats/watchedAuthors.ts b/packages/api/services/stats/watchedAuthors.ts index 2f582a14b..ee6ab630a 100644 --- a/packages/api/services/stats/watchedAuthors.ts +++ b/packages/api/services/stats/watchedAuthors.ts @@ -3,13 +3,6 @@ import { prismaClient as prismaDmStats } from "~prisma-schemas/schemas/dm_stats/ import type { UserServices } from "../../index"; import { getAuthorFullNames } from "../coa/authors"; -export interface AuthorDetails { - personcode: string; - missingStoryCount: number; - storyCount: number; - fullname: string; -} - const getStoryCountPerAuthor = async ( personcodes: string[], ): Promise<{ [personcode: string]: number }> => diff --git a/packages/prisma-schemas/schemas/coa/extended/index.ts b/packages/prisma-schemas/schemas/coa/extended/index.ts index 3f9fb29ba..0cca3ea09 100644 --- a/packages/prisma-schemas/schemas/coa/extended/index.ts +++ b/packages/prisma-schemas/schemas/coa/extended/index.ts @@ -8,7 +8,7 @@ import { computePublicationcode, } from "./overrideNullableCodes"; -export type Augmented = { +type Augmented = { issuecode: string; publicationcode: string; issuenumber: string;