@jskit-ai/kernel 0.1.55 → 0.1.56

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 (55) hide show
  1. package/package.json +3 -2
  2. package/server/actions/ActionRuntimeServiceProvider.test.js +23 -15
  3. package/server/http/lib/kernel.test.js +447 -0
  4. package/server/http/lib/routeRegistration.js +236 -15
  5. package/server/http/lib/routeTransport.js +126 -0
  6. package/server/http/lib/routeValidator.js +133 -198
  7. package/server/http/lib/routeValidator.test.js +385 -278
  8. package/server/http/lib/router.js +17 -2
  9. package/server/platform/providerRuntime.test.js +7 -7
  10. package/server/runtime/bootBootstrapRoutes.js +2 -18
  11. package/server/runtime/bootBootstrapRoutes.test.js +5 -14
  12. package/server/runtime/fastifyBootstrap.js +119 -0
  13. package/server/runtime/fastifyBootstrap.test.js +119 -1
  14. package/server/runtime/moduleConfig.js +32 -62
  15. package/server/runtime/moduleConfig.test.js +48 -24
  16. package/server/support/pageTargets.js +15 -9
  17. package/server/support/pageTargets.test.js +1 -1
  18. package/shared/actions/actionContributorHelpers.js +5 -11
  19. package/shared/actions/actionDefinitions.js +37 -150
  20. package/shared/actions/actionDefinitions.test.js +117 -136
  21. package/shared/actions/policies.js +25 -169
  22. package/shared/actions/policies.test.js +76 -87
  23. package/shared/actions/registry.test.js +24 -50
  24. package/shared/support/crudFieldContract.js +322 -0
  25. package/shared/support/crudFieldContract.test.js +67 -0
  26. package/shared/support/crudListFilters.js +582 -38
  27. package/shared/support/crudListFilters.test.js +178 -8
  28. package/shared/support/crudLookup.js +14 -7
  29. package/shared/support/crudLookup.test.js +91 -66
  30. package/shared/support/shellLayoutTargets.test.js +1 -1
  31. package/shared/validators/composeSchemaDefinitions.js +53 -0
  32. package/shared/validators/composeSchemaDefinitions.test.js +156 -0
  33. package/shared/validators/createCursorListValidator.js +22 -35
  34. package/shared/validators/createCursorListValidator.test.js +22 -23
  35. package/shared/validators/cursorPaginationQueryValidator.js +14 -24
  36. package/shared/validators/cursorPaginationQueryValidator.test.js +18 -8
  37. package/shared/validators/htmlTimeSchemas.js +6 -4
  38. package/shared/validators/index.js +15 -7
  39. package/shared/validators/jsonRestSchemaSupport.js +139 -0
  40. package/shared/validators/mergeObjectSchemas.js +44 -6
  41. package/shared/validators/mergeObjectSchemas.test.js +60 -35
  42. package/shared/validators/recordIdParamsValidator.js +19 -52
  43. package/shared/validators/recordIdParamsValidator.test.js +13 -8
  44. package/shared/validators/resourceRequiredMetadata.js +3 -3
  45. package/shared/validators/resourceRequiredMetadata.test.js +29 -16
  46. package/shared/validators/schemaDefinitions.js +126 -0
  47. package/shared/validators/schemaDefinitions.test.js +51 -0
  48. package/shared/validators/schemaPayloadValidation.js +65 -0
  49. package/test/barrelExposure.test.js +30 -0
  50. package/test/routeInputContractGuard.test.js +10 -6
  51. package/shared/validators/mergeValidators.js +0 -89
  52. package/shared/validators/mergeValidators.test.js +0 -116
  53. package/shared/validators/nestValidator.js +0 -53
  54. package/shared/validators/nestValidator.test.js +0 -60
  55. package/shared/validators/settingsFieldNormalization.js +0 -40
@@ -2,6 +2,7 @@ import { ensureNonEmptyText, normalizeObject, normalizeText } from "../../../sha
2
2
  import { RouteDefinitionError } from "./errors.js";
3
3
  import { resolveRouteValidatorOptions } from "./routeValidator.js";
4
4
  import { normalizeMiddlewareStack as normalizeSharedMiddlewareStack } from "./routeSupport.js";
5
+ import { normalizeRouteOutputTransform, normalizeRouteTransport } from "./routeTransport.js";
5
6
 
6
7
  function normalizeMethod(method) {
7
8
  return ensureNonEmptyText(method, "route method").toUpperCase();
@@ -79,8 +80,21 @@ class HttpRouter {
79
80
  const routeMiddleware = normalizeRouterMiddlewareStack(resolvedOptions.middleware, {
80
81
  context: `Route ${input.method} ${input.path} middleware`
81
82
  });
82
- const routeInput = Object.prototype.hasOwnProperty.call(resolvedOptions, "input") ? resolvedOptions.input : null;
83
- const routeOutput = Object.prototype.hasOwnProperty.call(resolvedOptions, "output") ? resolvedOptions.output : null;
83
+ const routeInput = Object.hasOwn(resolvedOptions, "input") ? resolvedOptions.input : null;
84
+ const routeOutput = normalizeRouteOutputTransform(
85
+ Object.hasOwn(resolvedOptions, "output") ? resolvedOptions.output : null,
86
+ {
87
+ context: `Route ${input.method} ${input.path} output`,
88
+ ErrorType: RouteDefinitionError
89
+ }
90
+ );
91
+ const routeTransport = normalizeRouteTransport(
92
+ Object.hasOwn(resolvedOptions, "transport") ? resolvedOptions.transport : null,
93
+ {
94
+ context: `Route ${input.method} ${input.path} transport`,
95
+ ErrorType: RouteDefinitionError
96
+ }
97
+ );
84
98
 
85
99
  const route = Object.freeze({
86
100
  id: normalizeText(resolvedOptions.id),
@@ -89,6 +103,7 @@ class HttpRouter {
89
103
  schema: resolvedOptions.schema,
90
104
  input: routeInput,
91
105
  output: routeOutput,
106
+ transport: routeTransport,
92
107
  config: normalizeObject(resolvedOptions.config),
93
108
  auth: resolvedOptions.auth,
94
109
  contextPolicy: resolvedOptions.contextPolicy,
@@ -112,17 +112,17 @@ test("createProviderRuntimeFromApp discovers package providers from descriptor d
112
112
  }
113
113
  });
114
114
 
115
- test("createProviderRuntimeFromApp ignores legacy app local src/server/providers folder", async () => {
116
- const appRoot = await createTestAppRoot("kernel-provider-runtime-legacy-app-local-");
115
+ test("createProviderRuntimeFromApp ignores app-local src/server/providers folder", async () => {
116
+ const appRoot = await createTestAppRoot("kernel-provider-runtime-app-local-");
117
117
  try {
118
118
  await mkdir(path.join(appRoot, "src", "server", "providers"), { recursive: true });
119
119
  await writeFile(
120
- path.join(appRoot, "src", "server", "providers", "LegacyProvider.js"),
120
+ path.join(appRoot, "src", "server", "providers", "IgnoredProvider.js"),
121
121
  [
122
- "export default class LegacyProvider {",
123
- " static id = \"legacy.app.local\";",
122
+ "export default class IgnoredProvider {",
123
+ " static id = \"ignored.app.local\";",
124
124
  " register(app) {",
125
- " app.instance(\"legacy.value\", true);",
125
+ " app.instance(\"ignored.value\", true);",
126
126
  " }",
127
127
  " boot() {}",
128
128
  "}"
@@ -139,7 +139,7 @@ test("createProviderRuntimeFromApp ignores legacy app local src/server/providers
139
139
  assert.deepEqual(runtime.providerPackageOrder, []);
140
140
  assert.equal(runtime.appLocalProviderOrder.length, 0);
141
141
  assert.deepEqual(runtime.diagnostics.providerOrder, ["runtime.actions", "runtime.server"]);
142
- assert.equal(runtime.app.has("legacy.value"), false);
142
+ assert.equal(runtime.app.has("ignored.value"), false);
143
143
  } finally {
144
144
  await rm(appRoot, { recursive: true, force: true });
145
145
  }
@@ -1,18 +1,6 @@
1
- import { Type } from "typebox";
2
- import { normalizeObjectInput } from "../../shared/validators/inputNormalization.js";
3
1
  import { AUTH_POLICY_PUBLIC } from "../../shared/support/policies.js";
4
2
  import { resolveBootstrapPayload } from "../registries/bootstrapPayloadContributorRegistry.js";
5
3
 
6
- const bootstrapQueryValidator = Object.freeze({
7
- schema: Type.Object({}, { additionalProperties: true }),
8
- normalize: normalizeObjectInput
9
- });
10
-
11
- const bootstrapOutputValidator = Object.freeze({
12
- schema: Type.Object({}, { additionalProperties: true }),
13
- normalize: normalizeObjectInput
14
- });
15
-
16
4
  function bootBootstrapRoutes(app) {
17
5
  const router = app.make("jskit.http.router");
18
6
 
@@ -24,17 +12,13 @@ function bootBootstrapRoutes(app) {
24
12
  meta: {
25
13
  tags: ["bootstrap"],
26
14
  summary: "Resolve app bootstrap payload from registered contributors"
27
- },
28
- queryValidator: bootstrapQueryValidator,
29
- responseValidators: {
30
- 200: bootstrapOutputValidator
31
15
  }
32
16
  },
33
17
  async function (request, reply) {
34
18
  const payload = await resolveBootstrapPayload(app, {
35
19
  request,
36
20
  reply,
37
- query: request.input?.query || {}
21
+ query: request.query || {}
38
22
  });
39
23
 
40
24
  reply.code(200).send(payload);
@@ -42,4 +26,4 @@ function bootBootstrapRoutes(app) {
42
26
  );
43
27
  }
44
28
 
45
- export { bootBootstrapRoutes, bootstrapQueryValidator, bootstrapOutputValidator };
29
+ export { bootBootstrapRoutes };
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createContainer } from "../container/index.js";
4
4
  import { registerBootstrapPayloadContributor } from "../registries/bootstrapPayloadContributorRegistry.js";
5
- import { bootBootstrapRoutes, bootstrapQueryValidator } from "./bootBootstrapRoutes.js";
5
+ import { bootBootstrapRoutes } from "./bootBootstrapRoutes.js";
6
6
 
7
7
  function createReplyDouble() {
8
8
  return {
@@ -19,14 +19,6 @@ function createReplyDouble() {
19
19
  };
20
20
  }
21
21
 
22
- test("bootstrapQueryValidator normalizes generic query payloads", () => {
23
- assert.deepEqual(bootstrapQueryValidator.normalize({}), {});
24
- assert.deepEqual(bootstrapQueryValidator.normalize({ workspaceSlug: " AcMe ", page: "1" }), {
25
- workspaceSlug: " AcMe ",
26
- page: "1"
27
- });
28
- });
29
-
30
22
  test("bootBootstrapRoutes registers GET /api/bootstrap and resolves contributors", async () => {
31
23
  const app = createContainer();
32
24
  const routes = [];
@@ -56,15 +48,14 @@ test("bootBootstrapRoutes registers GET /api/bootstrap and resolves contributors
56
48
 
57
49
  const bootstrapRoute = routes.find((entry) => entry.method === "GET" && entry.path === "/api/bootstrap");
58
50
  assert.ok(bootstrapRoute);
59
- assert.equal(typeof bootstrapRoute.route.queryValidator.normalize, "function");
51
+ assert.equal(Object.prototype.hasOwnProperty.call(bootstrapRoute.route, "query"), false);
52
+ assert.equal(Object.prototype.hasOwnProperty.call(bootstrapRoute.route, "responses"), false);
60
53
 
61
54
  const reply = createReplyDouble();
62
55
  await bootstrapRoute.handler(
63
56
  {
64
- input: {
65
- query: {
66
- workspaceSlug: "acme"
67
- }
57
+ query: {
58
+ workspaceSlug: "acme"
68
59
  }
69
60
  },
70
61
  reply
@@ -3,6 +3,9 @@ import { normalizeOpaqueId } from "../../shared/support/normalize.js";
3
3
  import { isAppError } from "./errors.js";
4
4
  import { resolveDefaultSurfaceId } from "../support/appConfig.js";
5
5
 
6
+ const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
7
+ const JSON_API_CONTENT_TYPE_PARSER_MARKER = Symbol.for("jskit.fastify.jsonApiContentTypeParserRegistered");
8
+
6
9
  function resolveLoggerLevel({ configuredLevel = "", nodeEnv = "development", allowedLevels = [] } = {}) {
7
10
  const normalizedConfiguredLevel = String(configuredLevel || "")
8
11
  .trim()
@@ -38,6 +41,48 @@ function createFastifyLoggerOptions({
38
41
  };
39
42
  }
40
43
 
44
+ function createFallbackJsonBodyParser() {
45
+ return function parseJsonBody(_request, body, done) {
46
+ const source = typeof body === "string" ? body.trim() : "";
47
+ if (!source) {
48
+ done(null, {});
49
+ return;
50
+ }
51
+
52
+ try {
53
+ done(null, JSON.parse(source));
54
+ } catch (error) {
55
+ if (error && typeof error === "object" && !Array.isArray(error)) {
56
+ error.statusCode = 400;
57
+ }
58
+ done(error);
59
+ }
60
+ };
61
+ }
62
+
63
+ function registerJsonApiContentTypeParser(fastify) {
64
+ if (!fastify || typeof fastify.addContentTypeParser !== "function") {
65
+ throw new TypeError("registerJsonApiContentTypeParser requires a Fastify instance.");
66
+ }
67
+
68
+ if (fastify[JSON_API_CONTENT_TYPE_PARSER_MARKER]) {
69
+ return false;
70
+ }
71
+
72
+ if (typeof fastify.hasContentTypeParser === "function" && fastify.hasContentTypeParser(JSON_API_CONTENT_TYPE)) {
73
+ fastify[JSON_API_CONTENT_TYPE_PARSER_MARKER] = true;
74
+ return false;
75
+ }
76
+
77
+ const parser = typeof fastify.getDefaultJsonParser === "function"
78
+ ? fastify.getDefaultJsonParser("ignore", "ignore")
79
+ : createFallbackJsonBodyParser();
80
+
81
+ fastify.addContentTypeParser(JSON_API_CONTENT_TYPE, { parseAs: "string" }, parser);
82
+ fastify[JSON_API_CONTENT_TYPE_PARSER_MARKER] = true;
83
+ return true;
84
+ }
85
+
41
86
  function registerRequestLoggingHooks(
42
87
  app,
43
88
  {
@@ -124,6 +169,61 @@ function resolveValidationFieldErrors(error) {
124
169
  return fieldErrors;
125
170
  }
126
171
 
172
+ function resolveRequestRouteTransport(request) {
173
+ const directTransport =
174
+ request?.routeTransport && typeof request.routeTransport === "object" && !Array.isArray(request.routeTransport)
175
+ ? request.routeTransport
176
+ : null;
177
+ if (directTransport) {
178
+ return directTransport;
179
+ }
180
+
181
+ const configTransport =
182
+ request?.routeOptions?.config?.transport &&
183
+ typeof request.routeOptions.config.transport === "object" &&
184
+ !Array.isArray(request.routeOptions.config.transport)
185
+ ? request.routeOptions.config.transport
186
+ : null;
187
+ const runtimeTransport =
188
+ configTransport?.runtime && typeof configTransport.runtime === "object" && !Array.isArray(configTransport.runtime)
189
+ ? configTransport.runtime
190
+ : null;
191
+ if (runtimeTransport) {
192
+ return runtimeTransport;
193
+ }
194
+
195
+ return configTransport;
196
+ }
197
+
198
+ function applyRouteTransportErrorResponse(reply, request, error, {
199
+ statusCode = 500,
200
+ normalizedErrorCode = ""
201
+ } = {}) {
202
+ const transport = resolveRequestRouteTransport(request);
203
+ const errorSerializer = transport && typeof transport.error === "function" ? transport.error : null;
204
+ if (!errorSerializer) {
205
+ return false;
206
+ }
207
+
208
+ const payload = errorSerializer(error, {
209
+ request,
210
+ reply,
211
+ statusCode,
212
+ code: normalizedErrorCode
213
+ });
214
+
215
+ if (payload && typeof payload.then === "function") {
216
+ throw new TypeError("Route transport error serializer must return synchronously.");
217
+ }
218
+
219
+ if (transport.contentType) {
220
+ reply.header("Content-Type", transport.contentType);
221
+ }
222
+
223
+ reply.code(statusCode).send(payload);
224
+ return true;
225
+ }
226
+
127
227
  function registerApiErrorHandler(
128
228
  app,
129
229
  {
@@ -158,6 +258,12 @@ function registerApiErrorHandler(
158
258
  if (Array.isArray(error?.validation)) {
159
259
  const fieldErrors = resolveValidationFieldErrors(error);
160
260
  const validationErrorCode = normalizedErrorCode || "validation_failed";
261
+ if (applyRouteTransportErrorResponse(reply, request, error, {
262
+ statusCode: 400,
263
+ normalizedErrorCode: validationErrorCode
264
+ })) {
265
+ return;
266
+ }
161
267
  reply.code(400).send({
162
268
  error: "Validation failed.",
163
269
  code: validationErrorCode,
@@ -170,6 +276,12 @@ function registerApiErrorHandler(
170
276
  }
171
277
 
172
278
  if (isAppError(error) || error instanceof ActionRuntimeError) {
279
+ if (applyRouteTransportErrorResponse(reply, request, error, {
280
+ statusCode: error.status,
281
+ normalizedErrorCode: normalizedErrorCode || "app_error"
282
+ })) {
283
+ return;
284
+ }
173
285
  if (error.status >= 500) {
174
286
  recordDbError(error);
175
287
  captureServerError(request, error, error.status);
@@ -222,6 +334,12 @@ function registerApiErrorHandler(
222
334
  code: normalizedErrorCode
223
335
  };
224
336
  }
337
+ if (applyRouteTransportErrorResponse(reply, request, error, {
338
+ statusCode,
339
+ normalizedErrorCode: fallbackErrorCode
340
+ })) {
341
+ return;
342
+ }
225
343
  reply.code(statusCode).send(payload);
226
344
  });
227
345
  }
@@ -364,6 +482,7 @@ async function runGracefulShutdown({
364
482
  export {
365
483
  resolveLoggerLevel,
366
484
  createFastifyLoggerOptions,
485
+ registerJsonApiContentTypeParser,
367
486
  registerRequestLoggingHooks,
368
487
  registerApiErrorHandler,
369
488
  ensureApiErrorHandling,
@@ -2,12 +2,13 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
4
  import { AppError, isAppError } from "./errors.js";
5
- import { registerApiErrorHandler, registerRequestLoggingHooks } from "./fastifyBootstrap.js";
5
+ import { registerApiErrorHandler, registerJsonApiContentTypeParser, registerRequestLoggingHooks } from "./fastifyBootstrap.js";
6
6
 
7
7
  function createFastifyStub() {
8
8
  return {
9
9
  errorHandler: null,
10
10
  hooks: {},
11
+ contentTypeParsers: new Map(),
11
12
  log: {
12
13
  error() {}
13
14
  },
@@ -16,6 +17,15 @@ function createFastifyStub() {
16
17
  },
17
18
  addHook(name, handler) {
18
19
  this.hooks[name] = handler;
20
+ },
21
+ hasContentTypeParser(contentType) {
22
+ return this.contentTypeParsers.has(String(contentType || "").trim().toLowerCase());
23
+ },
24
+ addContentTypeParser(contentType, options, parser) {
25
+ this.contentTypeParsers.set(String(contentType || "").trim().toLowerCase(), {
26
+ options,
27
+ parser
28
+ });
19
29
  }
20
30
  };
21
31
  }
@@ -157,6 +167,114 @@ test("registerApiErrorHandler keeps known error code for non-app errors", () =>
157
167
  });
158
168
  });
159
169
 
170
+ test("registerApiErrorHandler uses route transport error serializer when present", () => {
171
+ const fastify = createFastifyStub();
172
+ registerApiErrorHandler(fastify, { isAppError });
173
+
174
+ const reply = createReplyStub();
175
+ const error = new AppError(422, "Validation failed.", {
176
+ code: "invalid_contact",
177
+ details: {
178
+ fieldErrors: {
179
+ name: "Name is required."
180
+ }
181
+ }
182
+ });
183
+
184
+ fastify.errorHandler(
185
+ error,
186
+ {
187
+ routeTransport: {
188
+ contentType: "application/vnd.api+json",
189
+ error(currentError, { statusCode, code }) {
190
+ return {
191
+ errors: [
192
+ {
193
+ status: String(statusCode),
194
+ code,
195
+ title: currentError.message
196
+ }
197
+ ]
198
+ };
199
+ }
200
+ }
201
+ },
202
+ reply
203
+ );
204
+
205
+ assert.equal(reply.statusCode, 422);
206
+ assert.equal(reply.headers["Content-Type"], "application/vnd.api+json");
207
+ assert.deepEqual(reply.payload, {
208
+ errors: [
209
+ {
210
+ status: "422",
211
+ code: "invalid_contact",
212
+ title: "Validation failed."
213
+ }
214
+ ]
215
+ });
216
+ });
217
+
218
+ test("registerApiErrorHandler uses route config transport runtime serializer before handler attachment", () => {
219
+ const fastify = createFastifyStub();
220
+ registerApiErrorHandler(fastify, { isAppError });
221
+
222
+ const reply = createReplyStub();
223
+ const error = new AppError(401, "Authentication required.", {
224
+ code: "AUTH_POLICY_ERROR"
225
+ });
226
+
227
+ fastify.errorHandler(
228
+ error,
229
+ {
230
+ routeOptions: {
231
+ config: {
232
+ transport: {
233
+ kind: "jsonapi-resource",
234
+ contentType: "application/vnd.api+json",
235
+ runtime: {
236
+ contentType: "application/vnd.api+json",
237
+ error(currentError, { statusCode, code }) {
238
+ return {
239
+ errors: [
240
+ {
241
+ status: String(statusCode),
242
+ code,
243
+ title: currentError.message
244
+ }
245
+ ]
246
+ };
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ },
253
+ reply
254
+ );
255
+
256
+ assert.equal(reply.statusCode, 401);
257
+ assert.equal(reply.headers["Content-Type"], "application/vnd.api+json");
258
+ assert.deepEqual(reply.payload, {
259
+ errors: [
260
+ {
261
+ status: "401",
262
+ code: "AUTH_POLICY_ERROR",
263
+ title: "Authentication required."
264
+ }
265
+ ]
266
+ });
267
+ });
268
+
269
+ test("registerJsonApiContentTypeParser installs the JSON:API media type parser once", () => {
270
+ const fastify = createFastifyStub();
271
+
272
+ assert.equal(registerJsonApiContentTypeParser(fastify), true);
273
+ assert.equal(fastify.hasContentTypeParser("application/vnd.api+json"), true);
274
+ assert.equal(registerJsonApiContentTypeParser(fastify), false);
275
+ assert.equal(fastify.contentTypeParsers.size, 1);
276
+ });
277
+
160
278
  test("registerRequestLoggingHooks uses configured default surface when getSurface is absent", async () => {
161
279
  const fastify = createFastifyStub();
162
280
  let loggedPayload = null;
@@ -1,6 +1,6 @@
1
- import { Check, Errors, Parse } from "typebox/value";
2
1
  import { deepFreeze } from "../../shared/support/deepFreeze.js";
3
2
  import { normalizeObject, normalizeText } from "../../shared/support/normalize.js";
3
+ import { validateSchemaPayload } from "../../shared/validators/schemaPayloadValidation.js";
4
4
 
5
5
  function normalizeModuleId(value) {
6
6
  const moduleId = normalizeText(value);
@@ -17,46 +17,6 @@ function normalizeSchema(value) {
17
17
  return value;
18
18
  }
19
19
 
20
- function normalizeIssuePath(issue) {
21
- const fromInstancePath = normalizeText(issue?.instancePath || issue?.path).replace(/^\//, "").replace(/\//g, ".");
22
- if (fromInstancePath) {
23
- return fromInstancePath;
24
- }
25
-
26
- const missingProperty = normalizeText(issue?.params?.missingProperty);
27
- if (missingProperty) {
28
- return missingProperty;
29
- }
30
-
31
- const additionalProperties = issue?.params?.additionalProperties;
32
- if (Array.isArray(additionalProperties) && additionalProperties.length > 0) {
33
- const firstAdditional = normalizeText(additionalProperties[0]);
34
- if (firstAdditional) {
35
- return firstAdditional;
36
- }
37
- }
38
-
39
- const oneAdditional = normalizeText(additionalProperties);
40
- if (oneAdditional) {
41
- return oneAdditional;
42
- }
43
-
44
- return "(root)";
45
- }
46
-
47
- function normalizeIssueMessage(issue) {
48
- const message = normalizeText(issue?.message || issue?.error || issue?.description);
49
- return message || "Invalid value.";
50
- }
51
-
52
- function normalizeValidationIssues(rawIssues = []) {
53
- return rawIssues.map((issue) => ({
54
- path: normalizeIssuePath(issue),
55
- message: normalizeIssueMessage(issue),
56
- keyword: normalizeText(issue?.keyword)
57
- }));
58
- }
59
-
60
20
  function formatIssues(issues = []) {
61
21
  return issues
62
22
  .slice(0, 8)
@@ -142,28 +102,38 @@ class ModuleConfigError extends Error {
142
102
  }
143
103
 
144
104
  function validateSchemaOrThrow({ moduleId, schema, value, coerce = false }) {
145
- if (coerce) {
146
- try {
147
- return Parse(schema, value);
148
- } catch (cause) {
149
- const issues = normalizeValidationIssues([...Errors(schema, value)]);
150
- throw new ModuleConfigError(buildInvalidConfigMessage(moduleId, issues), {
151
- moduleId,
152
- issues,
153
- cause
154
- });
155
- }
156
- }
157
-
158
- if (Check(schema, value)) {
159
- return value;
105
+ const schemaDefinition = {
106
+ schema,
107
+ mode: "replace"
108
+ };
109
+
110
+ try {
111
+ return validateSchemaPayload(schemaDefinition, value, {
112
+ phase: "input",
113
+ context: `module config "${moduleId}"`
114
+ });
115
+ } catch (cause) {
116
+ const fieldErrors = normalizeObject(cause?.fieldErrors);
117
+ const issues = Object.keys(fieldErrors).length > 0
118
+ ? Object.keys(fieldErrors).map((path) => ({
119
+ path,
120
+ message: normalizeText(fieldErrors[path], { fallback: "Invalid value." }),
121
+ keyword: ""
122
+ }))
123
+ : [
124
+ {
125
+ path: "(root)",
126
+ message: normalizeText(cause?.message, { fallback: "Invalid value." }),
127
+ keyword: ""
128
+ }
129
+ ];
130
+
131
+ throw new ModuleConfigError(buildInvalidConfigMessage(moduleId, issues), {
132
+ moduleId,
133
+ issues,
134
+ cause
135
+ });
160
136
  }
161
-
162
- const issues = normalizeValidationIssues([...Errors(schema, value)]);
163
- throw new ModuleConfigError(buildInvalidConfigMessage(moduleId, issues), {
164
- moduleId,
165
- issues
166
- });
167
137
  }
168
138
 
169
139
  function defineModuleConfig({