@warlock.js/core 5.25.0 → 5.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/esm/cli/commands/doctor/checks/index.mjs +2 -0
  3. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  4. package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
  5. package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
  6. package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
  7. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  8. package/esm/cli/commands/doctor.command.mjs +5 -0
  9. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  10. package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
  11. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  12. package/esm/cli/commands/typings-generator.command.mjs +4 -1
  13. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  14. package/esm/database/migrate-action.mjs +2 -2
  15. package/esm/database/migrate-action.mjs.map +1 -1
  16. package/esm/dev-server/legacy-locales-warning.mjs +16 -0
  17. package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
  18. package/esm/dev-server/module-loader.mjs +16 -0
  19. package/esm/dev-server/module-loader.mjs.map +1 -1
  20. package/esm/dev-server/special-file-patterns.mjs +2 -2
  21. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  22. package/esm/dev-server/translation-keys-sources.mjs +7 -3
  23. package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
  24. package/esm/dev-server/type-generator.mjs +27 -4
  25. package/esm/dev-server/type-generator.mjs.map +1 -1
  26. package/esm/generations/features/access.feature.mjs +38 -18
  27. package/esm/generations/features/access.feature.mjs.map +1 -1
  28. package/esm/generations/features/bull-board.feature.mjs +2 -2
  29. package/esm/generations/features/bull-board.feature.mjs.map +1 -1
  30. package/esm/http/build-rate-limit-options.d.mts +8 -2
  31. package/esm/http/build-rate-limit-options.d.mts.map +1 -1
  32. package/esm/http/build-rate-limit-options.mjs +1 -0
  33. package/esm/http/build-rate-limit-options.mjs.map +1 -1
  34. package/esm/http/errors/errors.d.mts +1 -17
  35. package/esm/http/errors/errors.d.mts.map +1 -1
  36. package/esm/http/errors/errors.mjs +1 -20
  37. package/esm/http/errors/errors.mjs.map +1 -1
  38. package/esm/http/index.d.mts +2 -2
  39. package/esm/http/index.mjs +1 -1
  40. package/esm/http/middleware/inject-request-context.d.mts +1 -1
  41. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  42. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  43. package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
  44. package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
  45. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  46. package/esm/http/request.d.mts +7 -24
  47. package/esm/http/request.d.mts.map +1 -1
  48. package/esm/http/request.mjs +1 -22
  49. package/esm/http/request.mjs.map +1 -1
  50. package/esm/http/response.d.mts.map +1 -1
  51. package/esm/http/response.mjs +4 -2
  52. package/esm/http/response.mjs.map +1 -1
  53. package/esm/index.d.mts +5 -3
  54. package/esm/index.mjs +5 -2
  55. package/esm/localization/index.mjs +4 -0
  56. package/esm/localization/parse-locale-dictionary.d.mts +23 -0
  57. package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
  58. package/esm/localization/parse-locale-dictionary.mjs +215 -0
  59. package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
  60. package/esm/localization/register-locale-dictionary.d.mts +12 -0
  61. package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
  62. package/esm/localization/register-locale-dictionary.mjs +58 -0
  63. package/esm/localization/register-locale-dictionary.mjs.map +1 -0
  64. package/esm/production/production-builder.mjs +11 -2
  65. package/esm/production/production-builder.mjs.map +1 -1
  66. package/esm/router/types.d.mts +8 -0
  67. package/esm/router/types.d.mts.map +1 -1
  68. package/esm/tests/start-http-development-server.d.mts +10 -0
  69. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  70. package/esm/tests/start-http-development-server.mjs +16 -2
  71. package/esm/tests/start-http-development-server.mjs.map +1 -1
  72. package/esm/validation/validateAll.mjs +48 -1
  73. package/esm/validation/validateAll.mjs.map +1 -1
  74. package/llms-full.txt +118 -75
  75. package/llms.txt +1 -1
  76. package/package.json +20 -20
  77. package/skills/README.md +1 -1
  78. package/skills/create-controller/SKILL.md +4 -0
  79. package/skills/test-http/SKILL.md +9 -1
  80. package/skills/use-localization/SKILL.md +39 -26
@@ -42,6 +42,14 @@ type RequestHandlerValidation<TRequest extends Request = Request> = {
42
42
  * Validation schema
43
43
  */
44
44
  schema?: ObjectValidator;
45
+ /**
46
+ * Route-parameter schema. Its successful output replaces the matching
47
+ * request params, so `request.input()` sees coerced values.
48
+ *
49
+ * Unlike `schema`, this opt-in never changes how routes without a params
50
+ * schema expose their raw string params.
51
+ */
52
+ params?: ObjectValidator;
45
53
  };
46
54
  interface RequestControllerContract {
47
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/router/types.ts"],"mappings":"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"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/router/types.ts"],"mappings":"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"}
@@ -7,6 +7,16 @@ type StartHttpTestServerOptions = {
7
7
  * bootstraps the app itself, and that bootstrap re-reads `.env`.
8
8
  */
9
9
  port?: number;
10
+ /**
11
+ * Whether to boot the optional web connector.
12
+ *
13
+ * Defaults to `true` to preserve the full-stack test server. Pass `false`
14
+ * for API-only suites: the web connector's lazy delegate is skipped before
15
+ * `boot()`, so it never constructs its Vite-backed implementation.
16
+ *
17
+ * @default true
18
+ */
19
+ web?: boolean;
10
20
  };
11
21
  /**
12
22
  * Start the HTTP test server (minimal - no file watching)
@@ -1 +1 @@
1
- {"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";KAyBY,0BAAA;EAAA;;;;AAON;AA4EN;EA5EE,IAAI;AAAA;;;;;;AA4EsF;AAkG5F;;iBAlGsB,mBAAA,CAAoB,OAAA,GAAS,0BAAA,GAAkC,OAAO;;AAkGzC;AA6BnD;;iBA7BsB,kBAAA,IAAsB,OAAO;;AA6BhB;;iBAAnB,mBAAA"}
1
+ {"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";KAyBY,0BAAA;EAAA;;;;AAkBP;AAkGL;EA7GE,IAAA;;;;;;;AA6G0F;AA0G5F;;EA5ME,GAAG;AAAA;AA4M8C;AA6BnD;;;;AAAmC;;;AA7BgB,iBA1G7B,mBAAA,CAAoB,OAAA,GAAS,0BAAA,GAAkC,OAAO;;;;;iBA0GtE,kBAAA,IAAsB,OAAO;;;;iBA6BnC,mBAAA"}
@@ -25,6 +25,18 @@ import config from "@mongez/config";
25
25
  */
26
26
  let isServerRunning = false;
27
27
  /**
28
+ * Start one lifecycle phase while omitting the web connector.
29
+ *
30
+ * Kept here rather than changing the manager's public API: this is a
31
+ * test-server-only selection, and retaining the manager's boot-all / start-all
32
+ * ordering is essential for HTTP and socket connector wiring.
33
+ */
34
+ async function startTestServerPhase(phase) {
35
+ const connectors = connectorsManager.list().filter((connector) => connector.lifecyclePhase === phase).filter((connector) => connector.name !== "web");
36
+ for (const connector of connectors) await connector.boot();
37
+ for (const connector of connectors) await connector.start();
38
+ }
39
+ /**
28
40
  * Apply the caller's port and preflight the bind.
29
41
  *
30
42
  * Called at the last moment before the HTTP connector binds, and that timing is
@@ -73,11 +85,13 @@ async function startHttpTestServer(options = {}) {
73
85
  await filesOrchestrator.init();
74
86
  await filesOrchestrator.initializeAll();
75
87
  await loadConfigFiles(true);
76
- await connectorsManager.startPhase("early");
88
+ if (options.web === false) await startTestServerPhase("early");
89
+ else await connectorsManager.startPhase("early");
77
90
  await filesOrchestrator.moduleLoader.loadAll();
78
91
  await applyTestServerPort(options.port);
79
92
  await Application.runStartupValidators();
80
- await connectorsManager.startPhase("late");
93
+ if (options.web === false) await startTestServerPhase("late");
94
+ else await connectorsManager.startPhase("late");
81
95
  isServerRunning = true;
82
96
  } catch (error) {
83
97
  await unwindPartialStartup();
@@ -1 +1 @@
1
- {"version":3,"file":"start-http-development-server.mjs","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\n * HTTP Test Server\n *\n * Starts a minimal HTTP server for testing.\n * Unlike the full DevelopmentServer, this:\n * - Does NOT watch files\n * - Does NOT do HMR\n * - Only starts connectors needed for HTTP requests\n *\n * Used in Vitest's globalSetup to start server once for all test workers.\n */\nimport config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\n\nlet isServerRunning = false;\n\nexport type StartHttpTestServerOptions = {\n /**\n * Port the test server binds to, overriding `http.port`.\n *\n * Needed because the server cannot be moved from outside: this function\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\n */\n port?: number;\n};\n\n/**\n * Apply the caller's port and preflight the bind.\n *\n * Called at the last moment before the HTTP connector binds, and that timing is\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\n * by default), and `src/config/http.ts` then resolves its port from that store,\n * so anything the caller set earlier is gone by the time the port is read.\n * Writing into `config` here is also the only channel that works — assigning\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\n * and never falls back to `process.env`.\n */\nasync function applyTestServerPort(port?: number): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n if (port !== undefined) {\n throw new Error(\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\n );\n }\n\n return;\n }\n\n if (port !== undefined) {\n config.set(\"http.port\", port);\n }\n\n const resolvedPort = config.get(\"http.port\");\n\n // No configured port at all is fine: Fastify falls back to its own default and\n // the workers resolve the same one from their own config, so both ends agree\n // without the channel.\n //\n // A non-numeric port is NOT fine, and must fail loudly rather than skip the\n // preflight silently. `env()` coerces a value only when it round-trips exactly\n // (`String(Number(v)) === v`), so `HTTP_PORT=3999` and `HTTP_PORT=0` arrive as\n // numbers, but `03999`, `+3999`, `1e3` and anything with stray whitespace stay\n // strings. Letting those through used to reach `HttpConnector.start()`'s\n // `listen({ port })` unchanged — no preflight, no published port, and every\n // test worker resolving a URL from a channel nothing ever wrote to.\n if (typeof resolvedPort !== \"number\") {\n throw new Error(\n `startHttpTestServer() cannot use \\`http.port\\` value ${JSON.stringify(resolvedPort)} — it did not round-trip through Number() (env() only coerces a value when \\`String(Number(v)) === v\\`, so \"03999\", \"+3999\", \"1e3\", and values with stray whitespace stay strings). Fix \\`HTTP_PORT\\` in .env, or pass an explicit numeric port: startHttpTestServer({ port: 3999 }).`,\n );\n }\n\n // `0` is the OS's \"pick a free one for me\" idiom, and the test server cannot\n // support it. Preflighting 0 would bind some unrelated ephemeral port and pass\n // without proving anything, and there is nothing meaningful to publish: the\n // port Fastify ends up binding is never recorded — `HttpConnector.start()`\n // stores the port it ASKED for, not the one it got. Accepting 0 silently is\n // what left `getTestServerUrl()` resolving to `http://host:0` through its own\n // config fallback, so every worker request went to a port nothing listens on.\n if (resolvedPort <= 0) {\n throw new Error(\n `startHttpTestServer() cannot run on port ${resolvedPort}. Pass an explicit port — e.g. startHttpTestServer({ port: 3999 }) — or set \\`http.port\\` in your config. Test workers are separate processes that resolve the server's URL from the port published at startup, and an OS-assigned port is not knowable to them.`,\n );\n }\n\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\n\n publishTestServerPort(resolvedPort);\n}\n\n/**\n * Start the HTTP test server (minimal - no file watching)\n * Call this in Vitest's globalSetup\n *\n * @example\n * // run this suite on its own port, whatever `.env` says\n * await startHttpTestServer({ port: 3999 });\n */\nexport async function startHttpTestServer(options: StartHttpTestServerOptions = {}): Promise<void> {\n const nodeEnvironment = process.env.NODE_ENV;\n\n if (nodeEnvironment === undefined) {\n process.env.NODE_ENV = \"test\";\n } else if (nodeEnvironment !== \"test\") {\n throw new Error(\n `startHttpTestServer() requires NODE_ENV=\"test\", but received NODE_ENV=${JSON.stringify(nodeEnvironment)}. Set NODE_ENV=test before starting the test server.`,\n );\n }\n\n if (isServerRunning) {\n console.log(\"[test-server] Server already running, skipping start\");\n return;\n }\n\n console.log(\"[test-server] Starting HTTP test server...\");\n\n try {\n // `globalSetup` runs in Vitest's main process, where Vitest's worker env\n // setup has not run. Load `.env.test` here, before any config is read.\n await loadEnvironmentFiles();\n\n // Set environment\n Application.setRuntimeStrategy(\"development\");\n Application.setEnvironment(\"test\");\n\n // Bootstrap (env, etc.)\n await warlockConfigManager.load();\n await bootstrap();\n\n // Initialize file orchestrator (but don't watch)\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n\n // Load config files\n await loadConfigFiles(true);\n\n // Early-phase connectors (database, cache, logger, storage, mailer,\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\n // side-effect can query the DB at import time, so the data source has to\n // be registered first. This mirrors the dev/prod boot order (see\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n\n // Load application modules (their boot side-effects now see a live DB).\n await filesOrchestrator.moduleLoader.loadAll();\n\n await applyTestServerPort(options.port);\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — same slot as\n // `DevelopmentServer.start()` and the generated production `app.ts`.\n await Application.runStartupValidators();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered its routes and listeners.\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n\n isServerRunning = true;\n } catch (error) {\n // This function owns the connectors it starts, so a failure part-way\n // through has to leave nothing behind. Without this, `isServerRunning` was\n // still `false` — it is only set on the last line — so `globalTeardown`'s\n // `stopHttpTestServer()` reported \"No server to stop\" and walked away from\n // live early-phase connectors and a published port that outlived them.\n await unwindPartialStartup();\n\n // Rethrown unchanged: the startup failure is the one the caller needs to\n // read. A cleanup failure that replaced it would hide the actual cause.\n throw error;\n }\n}\n\n/**\n * Best-effort teardown of whatever a failed {@link startHttpTestServer} managed\n * to start.\n *\n * Every step is independent and none may throw: this runs while an error is\n * already in flight, and the caller is about to rethrow it.\n */\nasync function unwindPartialStartup(): Promise<void> {\n try {\n await connectorsManager.shutdown();\n } catch (shutdownError) {\n // Reported, not thrown — it is secondary to the startup error being\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(\"[test-server] Cleanup after a failed start did not complete:\", shutdownError);\n }\n\n withdrawTestServerPort();\n isServerRunning = false;\n}\n\n/**\n * Stop the HTTP test server\n * Call this in Vitest's globalTeardown\n */\nexport async function stopHttpTestServer(): Promise<void> {\n if (!isServerRunning) {\n console.log(\"[test-server] No server to stop\");\n return;\n }\n\n try {\n await connectorsManager.shutdown();\n\n console.log(\"[test-server] HTTP test server stopped\");\n } catch (error) {\n // Surfaced, never swallowed — cleaning up is not the same as the shutdown\n // having succeeded, and a suite whose teardown failed should say so.\n console.error(\"[test-server] Error stopping HTTP server:\", error);\n\n throw error;\n } finally {\n // In a `finally` rather than after the `await`: when `shutdown()` threw,\n // the published port survived the server it pointed at, so the next run in\n // the same process inherited a stale one and sent every request to it.\n withdrawTestServerPort();\n\n isServerRunning = false;\n }\n}\n\n/**\n * Check if test server is running\n */\nexport function isTestServerRunning(): boolean {\n return isServerRunning;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI,kBAAkB;;;;;;;;;;;;AAuBtB,eAAe,oBAAoB,MAA8B;CAG/D,IAAI,CAFe,OAAO,IAAI,MAEhB,GAAG;EACf,IAAI,SAAS,QACX,MAAM,IAAI,MACR,+BAA+B,KAAK,2HACtC;EAGF;CACF;CAEA,IAAI,SAAS,QACX,OAAO,IAAI,aAAa,IAAI;CAG9B,MAAM,eAAe,OAAO,IAAI,WAAW;CAa3C,IAAI,OAAO,iBAAiB,UAC1B,MAAM,IAAI,MACR,wDAAwD,KAAK,UAAU,YAAY,EAAE,sRACvF;CAUF,IAAI,gBAAgB,GAClB,MAAM,IAAI,MACR,4CAA4C,aAAa,iQAC3D;CAGF,MAAM,sBAAsB,cAAc,OAAO,IAAI,WAAW,KAAK,WAAW;CAEhF,sBAAsB,YAAY;AACpC;;;;;;;;;AAUA,eAAsB,oBAAoB,UAAsC,CAAC,GAAkB;CACjG,MAAM,kBAAkB,QAAQ,IAAI;CAEpC,IAAI,oBAAoB,QACtB,QAAQ,IAAI,WAAW;MAClB,IAAI,oBAAoB,QAC7B,MAAM,IAAI,MACR,yEAAyE,KAAK,UAAU,eAAe,EAAE,qDAC3G;CAGF,IAAI,iBAAiB;EACnB,QAAQ,IAAI,sDAAsD;EAClE;CACF;CAEA,QAAQ,IAAI,4CAA4C;CAExD,IAAI;EAGF,MAAM,qBAAqB;EAG3B,YAAY,mBAAmB,aAAa;EAC5C,YAAY,eAAe,MAAM;EAGjC,MAAM,qBAAqB,KAAK;EAChC,MAAM,UAAU;EAGhB,MAAM,kBAAkB,KAAK;EAC7B,MAAM,kBAAkB,cAAc;EAGtC,MAAM,gBAAgB,IAAI;EAO1B,MAAM,kBAAkB,kBAAwC;EAGhE,MAAM,kBAAkB,aAAa,QAAQ;EAE7C,MAAM,oBAAoB,QAAQ,IAAI;EAKtC,MAAM,YAAY,qBAAqB;EAIvC,MAAM,kBAAkB,iBAAuC;EAE/D,kBAAkB;CACpB,SAAS,OAAO;EAMd,MAAM,qBAAqB;EAI3B,MAAM;CACR;AACF;;;;;;;;AASA,eAAe,uBAAsC;CACnD,IAAI;EACF,MAAM,kBAAkB,SAAS;CACnC,SAAS,eAAe;EAGtB,QAAQ,MAAM,gEAAgE,aAAa;CAC7F;CAEA,uBAAuB;CACvB,kBAAkB;AACpB;;;;;AAMA,eAAsB,qBAAoC;CACxD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IAAI,iCAAiC;EAC7C;CACF;CAEA,IAAI;EACF,MAAM,kBAAkB,SAAS;EAEjC,QAAQ,IAAI,wCAAwC;CACtD,SAAS,OAAO;EAGd,QAAQ,MAAM,6CAA6C,KAAK;EAEhE,MAAM;CACR,UAAU;EAIR,uBAAuB;EAEvB,kBAAkB;CACpB;AACF;;;;AAKA,SAAgB,sBAA+B;CAC7C,OAAO;AACT"}
1
+ {"version":3,"file":"start-http-development-server.mjs","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\n * HTTP Test Server\n *\n * Starts a minimal HTTP server for testing.\n * Unlike the full DevelopmentServer, this:\n * - Does NOT watch files\n * - Does NOT do HMR\n * - Only starts connectors needed for HTTP requests\n *\n * Used in Vitest's globalSetup to start server once for all test workers.\n */\nimport config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\n\nlet isServerRunning = false;\n\nexport type StartHttpTestServerOptions = {\n /**\n * Port the test server binds to, overriding `http.port`.\n *\n * Needed because the server cannot be moved from outside: this function\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\n */\n port?: number;\n\n /**\n * Whether to boot the optional web connector.\n *\n * Defaults to `true` to preserve the full-stack test server. Pass `false`\n * for API-only suites: the web connector's lazy delegate is skipped before\n * `boot()`, so it never constructs its Vite-backed implementation.\n *\n * @default true\n */\n web?: boolean;\n};\n\n/**\n * Start one lifecycle phase while omitting the web connector.\n *\n * Kept here rather than changing the manager's public API: this is a\n * test-server-only selection, and retaining the manager's boot-all / start-all\n * ordering is essential for HTTP and socket connector wiring.\n */\nasync function startTestServerPhase(phase: ConnectorLifecyclePhase): Promise<void> {\n const connectors = connectorsManager\n .list()\n .filter((connector) => connector.lifecyclePhase === phase)\n .filter((connector) => connector.name !== \"web\");\n\n for (const connector of connectors) {\n await connector.boot();\n }\n\n for (const connector of connectors) {\n await connector.start();\n }\n}\n\n/**\n * Apply the caller's port and preflight the bind.\n *\n * Called at the last moment before the HTTP connector binds, and that timing is\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\n * by default), and `src/config/http.ts` then resolves its port from that store,\n * so anything the caller set earlier is gone by the time the port is read.\n * Writing into `config` here is also the only channel that works — assigning\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\n * and never falls back to `process.env`.\n */\nasync function applyTestServerPort(port?: number): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n if (port !== undefined) {\n throw new Error(\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\n );\n }\n\n return;\n }\n\n if (port !== undefined) {\n config.set(\"http.port\", port);\n }\n\n const resolvedPort = config.get(\"http.port\");\n\n // No configured port at all is fine: Fastify falls back to its own default and\n // the workers resolve the same one from their own config, so both ends agree\n // without the channel.\n //\n // A non-numeric port is NOT fine, and must fail loudly rather than skip the\n // preflight silently. `env()` coerces a value only when it round-trips exactly\n // (`String(Number(v)) === v`), so `HTTP_PORT=3999` and `HTTP_PORT=0` arrive as\n // numbers, but `03999`, `+3999`, `1e3` and anything with stray whitespace stay\n // strings. Letting those through used to reach `HttpConnector.start()`'s\n // `listen({ port })` unchanged — no preflight, no published port, and every\n // test worker resolving a URL from a channel nothing ever wrote to.\n if (typeof resolvedPort !== \"number\") {\n throw new Error(\n `startHttpTestServer() cannot use \\`http.port\\` value ${JSON.stringify(resolvedPort)} — it did not round-trip through Number() (env() only coerces a value when \\`String(Number(v)) === v\\`, so \"03999\", \"+3999\", \"1e3\", and values with stray whitespace stay strings). Fix \\`HTTP_PORT\\` in .env, or pass an explicit numeric port: startHttpTestServer({ port: 3999 }).`,\n );\n }\n\n // `0` is the OS's \"pick a free one for me\" idiom, and the test server cannot\n // support it. Preflighting 0 would bind some unrelated ephemeral port and pass\n // without proving anything, and there is nothing meaningful to publish: the\n // port Fastify ends up binding is never recorded — `HttpConnector.start()`\n // stores the port it ASKED for, not the one it got. Accepting 0 silently is\n // what left `getTestServerUrl()` resolving to `http://host:0` through its own\n // config fallback, so every worker request went to a port nothing listens on.\n if (resolvedPort <= 0) {\n throw new Error(\n `startHttpTestServer() cannot run on port ${resolvedPort}. Pass an explicit port — e.g. startHttpTestServer({ port: 3999 }) — or set \\`http.port\\` in your config. Test workers are separate processes that resolve the server's URL from the port published at startup, and an OS-assigned port is not knowable to them.`,\n );\n }\n\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\n\n publishTestServerPort(resolvedPort);\n}\n\n/**\n * Start the HTTP test server (minimal - no file watching)\n * Call this in Vitest's globalSetup\n *\n * @example\n * // run this suite on its own port, whatever `.env` says\n * await startHttpTestServer({ port: 3999 });\n */\nexport async function startHttpTestServer(options: StartHttpTestServerOptions = {}): Promise<void> {\n const nodeEnvironment = process.env.NODE_ENV;\n\n if (nodeEnvironment === undefined) {\n process.env.NODE_ENV = \"test\";\n } else if (nodeEnvironment !== \"test\") {\n throw new Error(\n `startHttpTestServer() requires NODE_ENV=\"test\", but received NODE_ENV=${JSON.stringify(nodeEnvironment)}. Set NODE_ENV=test before starting the test server.`,\n );\n }\n\n if (isServerRunning) {\n console.log(\"[test-server] Server already running, skipping start\");\n return;\n }\n\n console.log(\"[test-server] Starting HTTP test server...\");\n\n try {\n // `globalSetup` runs in Vitest's main process, where Vitest's worker env\n // setup has not run. Load `.env.test` here, before any config is read.\n await loadEnvironmentFiles();\n\n // Set environment\n Application.setRuntimeStrategy(\"development\");\n Application.setEnvironment(\"test\");\n\n // Bootstrap (env, etc.)\n await warlockConfigManager.load();\n await bootstrap();\n\n // Initialize file orchestrator (but don't watch)\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n\n // Load config files\n await loadConfigFiles(true);\n\n // Early-phase connectors (database, cache, logger, storage, mailer,\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\n // side-effect can query the DB at import time, so the data source has to\n // be registered first. This mirrors the dev/prod boot order (see\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\n if (options.web === false) {\n await startTestServerPhase(ConnectorLifecyclePhase.Early);\n } else {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n }\n\n // Load application modules (their boot side-effects now see a live DB).\n await filesOrchestrator.moduleLoader.loadAll();\n\n await applyTestServerPort(options.port);\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — same slot as\n // `DevelopmentServer.start()` and the generated production `app.ts`.\n await Application.runStartupValidators();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered its routes and listeners.\n if (options.web === false) {\n await startTestServerPhase(ConnectorLifecyclePhase.Late);\n } else {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n }\n\n isServerRunning = true;\n } catch (error) {\n // This function owns the connectors it starts, so a failure part-way\n // through has to leave nothing behind. Without this, `isServerRunning` was\n // still `false` — it is only set on the last line — so `globalTeardown`'s\n // `stopHttpTestServer()` reported \"No server to stop\" and walked away from\n // live early-phase connectors and a published port that outlived them.\n await unwindPartialStartup();\n\n // Rethrown unchanged: the startup failure is the one the caller needs to\n // read. A cleanup failure that replaced it would hide the actual cause.\n throw error;\n }\n}\n\n/**\n * Best-effort teardown of whatever a failed {@link startHttpTestServer} managed\n * to start.\n *\n * Every step is independent and none may throw: this runs while an error is\n * already in flight, and the caller is about to rethrow it.\n */\nasync function unwindPartialStartup(): Promise<void> {\n try {\n await connectorsManager.shutdown();\n } catch (shutdownError) {\n // Reported, not thrown — it is secondary to the startup error being\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(\"[test-server] Cleanup after a failed start did not complete:\", shutdownError);\n }\n\n withdrawTestServerPort();\n isServerRunning = false;\n}\n\n/**\n * Stop the HTTP test server\n * Call this in Vitest's globalTeardown\n */\nexport async function stopHttpTestServer(): Promise<void> {\n if (!isServerRunning) {\n console.log(\"[test-server] No server to stop\");\n return;\n }\n\n try {\n await connectorsManager.shutdown();\n\n console.log(\"[test-server] HTTP test server stopped\");\n } catch (error) {\n // Surfaced, never swallowed — cleaning up is not the same as the shutdown\n // having succeeded, and a suite whose teardown failed should say so.\n console.error(\"[test-server] Error stopping HTTP server:\", error);\n\n throw error;\n } finally {\n // In a `finally` rather than after the `await`: when `shutdown()` threw,\n // the published port survived the server it pointed at, so the next run in\n // the same process inherited a stale one and sent every request to it.\n withdrawTestServerPort();\n\n isServerRunning = false;\n }\n}\n\n/**\n * Check if test server is running\n */\nexport function isTestServerRunning(): boolean {\n return isServerRunning;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI,kBAAkB;;;;;;;;AA8BtB,eAAe,qBAAqB,OAA+C;CACjF,MAAM,aAAa,kBAChB,KAAK,CAAC,CACN,QAAQ,cAAc,UAAU,mBAAmB,KAAK,CAAC,CACzD,QAAQ,cAAc,UAAU,SAAS,KAAK;CAEjD,KAAK,MAAM,aAAa,YACtB,MAAM,UAAU,KAAK;CAGvB,KAAK,MAAM,aAAa,YACtB,MAAM,UAAU,MAAM;AAE1B;;;;;;;;;;;;AAaA,eAAe,oBAAoB,MAA8B;CAG/D,IAAI,CAFe,OAAO,IAAI,MAEhB,GAAG;EACf,IAAI,SAAS,QACX,MAAM,IAAI,MACR,+BAA+B,KAAK,2HACtC;EAGF;CACF;CAEA,IAAI,SAAS,QACX,OAAO,IAAI,aAAa,IAAI;CAG9B,MAAM,eAAe,OAAO,IAAI,WAAW;CAa3C,IAAI,OAAO,iBAAiB,UAC1B,MAAM,IAAI,MACR,wDAAwD,KAAK,UAAU,YAAY,EAAE,sRACvF;CAUF,IAAI,gBAAgB,GAClB,MAAM,IAAI,MACR,4CAA4C,aAAa,iQAC3D;CAGF,MAAM,sBAAsB,cAAc,OAAO,IAAI,WAAW,KAAK,WAAW;CAEhF,sBAAsB,YAAY;AACpC;;;;;;;;;AAUA,eAAsB,oBAAoB,UAAsC,CAAC,GAAkB;CACjG,MAAM,kBAAkB,QAAQ,IAAI;CAEpC,IAAI,oBAAoB,QACtB,QAAQ,IAAI,WAAW;MAClB,IAAI,oBAAoB,QAC7B,MAAM,IAAI,MACR,yEAAyE,KAAK,UAAU,eAAe,EAAE,qDAC3G;CAGF,IAAI,iBAAiB;EACnB,QAAQ,IAAI,sDAAsD;EAClE;CACF;CAEA,QAAQ,IAAI,4CAA4C;CAExD,IAAI;EAGF,MAAM,qBAAqB;EAG3B,YAAY,mBAAmB,aAAa;EAC5C,YAAY,eAAe,MAAM;EAGjC,MAAM,qBAAqB,KAAK;EAChC,MAAM,UAAU;EAGhB,MAAM,kBAAkB,KAAK;EAC7B,MAAM,kBAAkB,cAAc;EAGtC,MAAM,gBAAgB,IAAI;EAO1B,IAAI,QAAQ,QAAQ,OAClB,MAAM,4BAAkD;OAExD,MAAM,kBAAkB,kBAAwC;EAIlE,MAAM,kBAAkB,aAAa,QAAQ;EAE7C,MAAM,oBAAoB,QAAQ,IAAI;EAKtC,MAAM,YAAY,qBAAqB;EAIvC,IAAI,QAAQ,QAAQ,OAClB,MAAM,2BAAiD;OAEvD,MAAM,kBAAkB,iBAAuC;EAGjE,kBAAkB;CACpB,SAAS,OAAO;EAMd,MAAM,qBAAqB;EAI3B,MAAM;CACR;AACF;;;;;;;;AASA,eAAe,uBAAsC;CACnD,IAAI;EACF,MAAM,kBAAkB,SAAS;CACnC,SAAS,eAAe;EAGtB,QAAQ,MAAM,gEAAgE,aAAa;CAC7F;CAEA,uBAAuB;CACvB,kBAAkB;AACpB;;;;;AAMA,eAAsB,qBAAoC;CACxD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IAAI,iCAAiC;EAC7C;CACF;CAEA,IAAI;EACF,MAAM,kBAAkB,SAAS;EAEjC,QAAQ,IAAI,wCAAwC;CACtD,SAAS,OAAO;EAGd,QAAQ,MAAM,6CAA6C,KAAK;EAEhE,MAAM;CACR,UAAU;EAIR,uBAAuB;EAEvB,kBAAkB;CACpB;AACF;;;;AAKA,SAAgB,sBAA+B;CAC7C,OAAO;AACT"}
@@ -23,27 +23,74 @@ function resolveDataToParse(validating, schema, request) {
23
23
  return data;
24
24
  }
25
25
  /**
26
+ * Route segments arrive from Fastify as strings. A params schema is an
27
+ * explicit request to parse those segments, so enable Seal's built-in
28
+ * coercion for validators that support it (numbers and booleans) without
29
+ * mutating the application-owned schema.
30
+ *
31
+ * This is deliberately duck-typed: a route may carry a Seal schema from a
32
+ * linked application copy rather than core's installed copy.
33
+ */
34
+ const coercingParamsSchemas = /* @__PURE__ */ new WeakMap();
35
+ function coercingParamsSchema(schema) {
36
+ const cached = coercingParamsSchemas.get(schema);
37
+ if (cached) return cached;
38
+ const coercing = buildCoercingParamsSchema(schema);
39
+ coercingParamsSchemas.set(schema, coercing);
40
+ return coercing;
41
+ }
42
+ function buildCoercingParamsSchema(schema) {
43
+ const cloned = schema.clone?.() ?? schema;
44
+ const shape = cloned.schema;
45
+ if (!shape) return cloned;
46
+ for (const [key, validator] of Object.entries(shape)) if (typeof validator.coerce === "function") shape[key] = validator.coerce();
47
+ return cloned;
48
+ }
49
+ /**
26
50
  * Validate the request route
27
51
  */
28
52
  async function validateAll(validation, request, response) {
29
53
  if (!validation) return;
30
54
  log.info("validation", "started", "Start validating the request");
55
+ let validatedData;
31
56
  if (validation.schema) {
32
57
  log.info("validation", "schema", "Validating request schema");
33
58
  try {
34
59
  const data = resolveDataToParse(validation.validating, validation.schema, request);
35
60
  const result = await v.validate(validation.schema, data);
36
- if (result.data && result.isValid) request.setValidatedData(result.data);
37
61
  if (!result.isValid) {
38
62
  log.warn("validation", "schema", "Schema Validation failed");
39
63
  return response.failedSchema(result);
40
64
  }
65
+ if (result.data) validatedData = result.data;
41
66
  log.success("validation", "schema", "Schema Validation passed");
42
67
  } catch (error) {
43
68
  log.warn("app.validation", "error", error);
44
69
  throw error;
45
70
  }
46
71
  }
72
+ if (validation.params) {
73
+ log.info("validation", "params", "Validating route params schema");
74
+ try {
75
+ const result = await v.validate(coercingParamsSchema(validation.params), request.params);
76
+ if (!result.isValid) {
77
+ log.warn("validation", "params", "Route params schema validation failed");
78
+ return response.failedSchema(result);
79
+ }
80
+ if (result.data) {
81
+ for (const [key, value] of Object.entries(result.data)) request.setParam(key, value);
82
+ validatedData = {
83
+ ...validatedData,
84
+ ...result.data
85
+ };
86
+ }
87
+ log.success("validation", "params", "Route params schema validation passed");
88
+ } catch (error) {
89
+ log.warn("app.validation", "error", error);
90
+ throw error;
91
+ }
92
+ }
93
+ if (validatedData) request.setValidatedData(validatedData);
47
94
  if (validation.validate) {
48
95
  const result = await validation.validate({
49
96
  request,
@@ -1 +1 @@
1
- {"version":3,"file":"validateAll.mjs","names":[],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request } from \"../http\";\nimport { Response } from \"../http/response\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(\n validating: RequestHandlerValidation[\"validating\"],\n schema: RequestHandlerValidation[\"schema\"],\n request: Request,\n) {\n if (!validating || validating.length === 0) {\n const data = request.allExceptParams();\n\n // Duck-typed rather than `instanceof ObjectValidator`: the app's schema can\n // come from a different copy of seal than core's, and `instanceof` would\n // then silently fall back to ignoring params.\n const shape = (schema as { schema?: unknown } | undefined)?.schema;\n\n if (!shape || typeof shape !== \"object\") return data;\n\n for (const [key, value] of Object.entries(request.params)) {\n if (data[key] === undefined && Object.hasOwn(shape, key)) {\n data[key] = value;\n }\n }\n\n return data;\n }\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, validation.schema, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate({ request, response });\n\n // if there is a result, it means it failed\n if (result) {\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n // a returned Response already carries its own status; anything else is\n // sent with the configured failure status (400 by default)\n if (result instanceof Response) return result;\n\n return response.send(result, config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;;AAQA,SAAS,mBACP,YACA,QACA,SACA;CACA,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG;EAC1C,MAAM,OAAO,QAAQ,gBAAgB;EAKrC,MAAM,QAAS,QAA6C;EAE5D,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;EAEhD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,MAAM,GACtD,IAAI,KAAK,SAAS,UAAa,OAAO,OAAO,OAAO,GAAG,GACrD,KAAK,OAAO;EAIhB,OAAO;CACT;CAEA,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,WAAW,QAAQ,OAAO;GACjF,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS;GAAE;GAAS;EAAS,CAAC;EAG9D,IAAI,QAAQ;GACV,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAIpD,IAAI,kBAAkB,UAAU,OAAO;GAEvC,OAAO,SAAS,KAAK,QAAQ,OAAO,IAAI,6BAA6B,GAAG,CAAC;EAC3E;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
1
+ {"version":3,"file":"validateAll.mjs","names":["candidate"],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v, type ObjectValidator } from \"@warlock.js/seal\";\nimport type { Request } from \"../http\";\nimport { Response } from \"../http/response\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(\n validating: RequestHandlerValidation[\"validating\"],\n schema: RequestHandlerValidation[\"schema\"],\n request: Request,\n) {\n if (!validating || validating.length === 0) {\n const data = request.allExceptParams();\n\n // Duck-typed rather than `instanceof ObjectValidator`: the app's schema can\n // come from a different copy of seal than core's, and `instanceof` would\n // then silently fall back to ignoring params.\n const shape = (schema as { schema?: unknown } | undefined)?.schema;\n\n if (!shape || typeof shape !== \"object\") return data;\n\n for (const [key, value] of Object.entries(request.params)) {\n if (data[key] === undefined && Object.hasOwn(shape, key)) {\n data[key] = value;\n }\n }\n\n return data;\n }\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Route segments arrive from Fastify as strings. A params schema is an\n * explicit request to parse those segments, so enable Seal's built-in\n * coercion for validators that support it (numbers and booleans) without\n * mutating the application-owned schema.\n *\n * This is deliberately duck-typed: a route may carry a Seal schema from a\n * linked application copy rather than core's installed copy.\n */\nconst coercingParamsSchemas = new WeakMap<ObjectValidator, ObjectValidator>();\n\nfunction coercingParamsSchema(schema: ObjectValidator): ObjectValidator {\n const cached = coercingParamsSchemas.get(schema);\n\n if (cached) return cached;\n\n const coercing = buildCoercingParamsSchema(schema);\n\n coercingParamsSchemas.set(schema, coercing);\n\n return coercing;\n}\n\nfunction buildCoercingParamsSchema(schema: ObjectValidator): ObjectValidator {\n const candidate = schema as ObjectValidator & {\n clone?: () => ObjectValidator;\n schema?: Record<string, { coerce?: () => unknown }>;\n };\n const cloned = candidate.clone?.() ?? schema;\n const shape = (cloned as typeof candidate).schema;\n\n if (!shape) return cloned;\n\n for (const [key, validator] of Object.entries(shape)) {\n if (typeof validator.coerce === \"function\") {\n shape[key] = validator.coerce() as typeof validator;\n }\n }\n\n return cloned;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n let validatedData: Record<string, unknown> | undefined;\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, validation.schema, request);\n const result = await v.validate(validation.schema, data);\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n if (result.data) {\n validatedData = result.data;\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.params) {\n log.info(\"validation\", \"params\", \"Validating route params schema\");\n try {\n const result = await v.validate(coercingParamsSchema(validation.params), request.params);\n\n if (!result.isValid) {\n log.warn(\"validation\", \"params\", \"Route params schema validation failed\");\n return response.failedSchema(result);\n }\n\n if (result.data) {\n for (const [key, value] of Object.entries(result.data)) {\n request.setParam(key, value);\n }\n\n validatedData = { ...validatedData, ...result.data };\n }\n\n log.success(\"validation\", \"params\", \"Route params schema validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validatedData) {\n request.setValidatedData(validatedData);\n }\n\n if (validation.validate) {\n const result = await validation.validate({ request, response });\n\n // if there is a result, it means it failed\n if (result) {\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n // a returned Response already carries its own status; anything else is\n // sent with the configured failure status (400 by default)\n if (result instanceof Response) return result;\n\n return response.send(result, config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":"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"}