@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
@@ -1,125 +1,65 @@
1
1
  import { normalizeObject, normalizeText } from "../../../shared/support/normalize.js";
2
- import { mergeValidators } from "../../../shared/validators/mergeValidators.js";
2
+ import {
3
+ normalizeSchemaDefinition,
4
+ resolveSchemaTransportSchemaDefinition,
5
+ validateSchemaPayload
6
+ } from "../../../shared/validators/index.js";
3
7
  import { RouteDefinitionError } from "./errors.js";
4
8
  import { resolveRouteLabel } from "./routeSupport.js";
5
9
 
6
10
  const ROUTE_VALIDATOR_SYMBOL = "@jskit-ai/kernel/http/routeValidator";
11
+ const JSON_REST_TRANSPORT_EXTENSION_KEY = "x-json-rest-schema";
7
12
  const VALIDATOR_OPTION_KEYS = Object.freeze([
8
13
  "meta",
9
- "bodyValidator",
10
- "queryValidator",
11
- "paramsValidator",
12
- "responseValidators",
14
+ "body",
15
+ "query",
16
+ "params",
17
+ "responses",
13
18
  "advanced"
14
19
  ]);
20
+ const ADVANCED_VALIDATOR_OPTION_KEYS = Object.freeze([
21
+ "fastifySchema"
22
+ ]);
23
+ const UNSUPPORTED_ROUTE_VALIDATOR_KEYS = Object.freeze([
24
+ "schema",
25
+ "input",
26
+ "validator"
27
+ ]);
15
28
 
16
- function passThroughInputSection(value) {
17
- return value;
18
- }
19
-
20
- function normalizeOptionalValidatorTransformer(source, normalized, { context = "route validator" } = {}) {
21
- if (!Object.prototype.hasOwnProperty.call(source, "normalize")) {
22
- return;
23
- }
24
-
25
- const normalize = source.normalize;
26
- if (normalize != null && typeof normalize !== "function") {
27
- throw new RouteDefinitionError(`${context}.normalize must be a function.`);
28
- }
29
- if (typeof normalize === "function") {
30
- normalized.normalize = normalize;
31
- }
32
- }
33
-
34
- function normalizeSingleRouteValidator(value, { context = "route validator" } = {}) {
35
- if (value == null) {
36
- return Object.freeze({});
37
- }
38
-
39
- if (!value || typeof value !== "object" || Array.isArray(value)) {
40
- throw new RouteDefinitionError(`${context} must be an object.`);
29
+ function stripJsonRestTransportExtensions(value) {
30
+ if (Array.isArray(value)) {
31
+ return value.map((entry) => stripJsonRestTransportExtensions(entry));
41
32
  }
42
33
 
43
- const source = normalizeObject(value);
44
- const normalized = {};
45
-
46
- if (Object.prototype.hasOwnProperty.call(source, "schema")) {
47
- normalized.schema = source.schema;
34
+ if (!value || typeof value !== "object") {
35
+ return value;
48
36
  }
49
37
 
50
- normalizeOptionalValidatorTransformer(source, normalized, { context });
51
-
52
- return Object.freeze(normalized);
53
- }
54
-
55
- function mergeNormalizedRouteValidators(validators, { context = "route validator" } = {}) {
56
- return mergeValidators(validators, {
57
- context,
58
- allowAsyncNormalize: false,
59
- createError(message) {
60
- return new RouteDefinitionError(message);
61
- }
62
- });
63
- }
38
+ const sanitized = {};
64
39
 
65
- function normalizeRouteValidator(value, { context = "route validator", allowArray = false } = {}) {
66
- if (value == null) {
67
- return Object.freeze({});
68
- }
69
-
70
- if (Array.isArray(value)) {
71
- if (!allowArray) {
72
- throw new RouteDefinitionError(`${context} does not support arrays.`);
73
- }
74
-
75
- if (value.length === 0) {
76
- return Object.freeze({});
40
+ for (const [key, entry] of Object.entries(value)) {
41
+ if (key === JSON_REST_TRANSPORT_EXTENSION_KEY) {
42
+ continue;
77
43
  }
78
44
 
79
- const validators = value.map((entry, index) => {
80
- const validator = normalizeSingleRouteValidator(entry, {
81
- context: `${context}[${index}]`
82
- });
83
-
84
- if (
85
- !Object.prototype.hasOwnProperty.call(validator, "schema") &&
86
- !Object.prototype.hasOwnProperty.call(validator, "normalize")
87
- ) {
88
- throw new RouteDefinitionError(`${context}[${index}] must define schema and/or normalize.`);
89
- }
90
-
91
- return validator;
92
- });
93
-
94
- return mergeNormalizedRouteValidators(validators, {
95
- context
96
- });
45
+ sanitized[key] = stripJsonRestTransportExtensions(entry);
97
46
  }
98
47
 
99
- return normalizeSingleRouteValidator(value, {
100
- context
101
- });
48
+ return sanitized;
102
49
  }
103
50
 
104
- function normalizeResponseValidatorEntry(value, { context = "route validator response entry" } = {}) {
105
- if (!value || typeof value !== "object" || Array.isArray(value)) {
106
- throw new RouteDefinitionError(`${context} must be an object.`);
107
- }
108
-
109
- const source = normalizeObject(value);
110
- const normalized = {};
111
-
112
- if (!Object.prototype.hasOwnProperty.call(source, "schema")) {
113
- throw new RouteDefinitionError(`${context}.schema is required when using a response validator object.`);
51
+ function normalizeRouteSchemaSection(value, { context = "route section", defaultMode = "patch" } = {}) {
52
+ try {
53
+ return normalizeSchemaDefinition(value, {
54
+ context,
55
+ defaultMode
56
+ });
57
+ } catch (error) {
58
+ throw new RouteDefinitionError(error?.message || `${context} is invalid.`);
114
59
  }
115
- normalized.schema = source.schema;
116
-
117
- normalizeOptionalValidatorTransformer(source, normalized, { context });
118
-
119
- return Object.freeze(normalized);
120
60
  }
121
61
 
122
- function normalizeResponseValidatorDefinition(value, { context = "route validator.response" } = {}) {
62
+ function normalizeResponseDefinition(value, { context = "route responses" } = {}) {
123
63
  if (value == null) {
124
64
  return undefined;
125
65
  }
@@ -132,8 +72,24 @@ function normalizeResponseValidatorDefinition(value, { context = "route validato
132
72
  const normalized = {};
133
73
 
134
74
  for (const [statusCode, entry] of Object.entries(source)) {
135
- normalized[statusCode] = normalizeResponseValidatorEntry(entry, {
136
- context: `${context}.${statusCode}`
75
+ const entryContext = `${context}.${statusCode}`;
76
+ if (
77
+ entry &&
78
+ typeof entry === "object" &&
79
+ !Array.isArray(entry) &&
80
+ Object.prototype.hasOwnProperty.call(entry, "transportSchema")
81
+ ) {
82
+ normalized[statusCode] = Object.freeze({
83
+ transportSchema: normalizeObject(entry.transportSchema, {
84
+ fallback: {}
85
+ })
86
+ });
87
+ continue;
88
+ }
89
+
90
+ normalized[statusCode] = normalizeRouteSchemaSection(entry, {
91
+ context: entryContext,
92
+ defaultMode: "replace"
137
93
  });
138
94
  }
139
95
 
@@ -155,46 +111,6 @@ function normalizeAdvancedFastifySchema(value, { context = "route validator" } =
155
111
  });
156
112
  }
157
113
 
158
- function normalizeAdvancedJskitInput(value, { context = "route validator" } = {}) {
159
- if (!Object.prototype.hasOwnProperty.call(value, "jskitInput")) {
160
- return undefined;
161
- }
162
-
163
- const jskitInput = value.jskitInput;
164
- if (!jskitInput || typeof jskitInput !== "object" || Array.isArray(jskitInput)) {
165
- throw new RouteDefinitionError(`${context}.advanced.jskitInput must be an object.`);
166
- }
167
-
168
- const supportedKeys = new Set(["body", "query", "params"]);
169
- for (const key of Object.keys(jskitInput)) {
170
- if (!supportedKeys.has(key)) {
171
- throw new RouteDefinitionError(
172
- `${context}.advanced.jskitInput.${key} is not supported. Use body, query, or params.`
173
- );
174
- }
175
- }
176
-
177
- const normalized = {};
178
- for (const key of ["body", "query", "params"]) {
179
- if (!Object.prototype.hasOwnProperty.call(jskitInput, key)) {
180
- continue;
181
- }
182
-
183
- const transform = jskitInput[key];
184
- if (transform == null) {
185
- continue;
186
- }
187
-
188
- if (typeof transform !== "function") {
189
- throw new RouteDefinitionError(`${context}.advanced.jskitInput.${key} must be a function.`);
190
- }
191
-
192
- normalized[key] = transform;
193
- }
194
-
195
- return Object.freeze(normalized);
196
- }
197
-
198
114
  function normalizeRouteValidatorMeta(value, { context = "route validator" } = {}) {
199
115
  if (value == null) {
200
116
  return Object.freeze({});
@@ -243,32 +159,23 @@ function normalizeRouteValidatorDefinition(sourceDefinition, { context = "route
243
159
  throw new RouteDefinitionError(`${context} must be an object.`);
244
160
  }
245
161
 
246
- if (Object.prototype.hasOwnProperty.call(definition, "body")) {
247
- throw new RouteDefinitionError(`${context}.body is not supported. Use ${context}.bodyValidator.`);
248
- }
249
- if (Object.prototype.hasOwnProperty.call(definition, "query")) {
250
- throw new RouteDefinitionError(`${context}.query is not supported. Use ${context}.queryValidator.`);
251
- }
252
- if (Object.prototype.hasOwnProperty.call(definition, "params")) {
253
- throw new RouteDefinitionError(`${context}.params is not supported. Use ${context}.paramsValidator.`);
254
- }
255
- if (Object.prototype.hasOwnProperty.call(definition, "response")) {
256
- throw new RouteDefinitionError(`${context}.response is not supported. Use ${context}.responseValidators.`);
162
+ const unsupportedKeys = Object.keys(definition).filter((key) => !VALIDATOR_OPTION_KEYS.includes(key));
163
+ if (unsupportedKeys.length > 0) {
164
+ throw new RouteDefinitionError(`${context}.${unsupportedKeys[0]} is not supported.`);
257
165
  }
258
166
 
259
167
  const meta = normalizeRouteValidatorMeta(definition.meta, {
260
168
  context
261
169
  });
262
- const bodyValidator = normalizeRouteValidator(definition.bodyValidator, {
263
- context: `${context}.bodyValidator`
170
+ const body = normalizeRouteSchemaSection(definition.body, {
171
+ context: `${context}.body`,
172
+ defaultMode: "patch"
264
173
  });
265
- const queryValidator = normalizeRouteValidator(definition.queryValidator, {
266
- context: `${context}.queryValidator`,
267
- allowArray: true
174
+ const query = normalizeRouteSchemaSection(definition.query, {
175
+ context: `${context}.query`
268
176
  });
269
- const paramsValidator = normalizeRouteValidator(definition.paramsValidator, {
270
- context: `${context}.paramsValidator`,
271
- allowArray: true
177
+ const params = normalizeRouteSchemaSection(definition.params, {
178
+ context: `${context}.params`
272
179
  });
273
180
 
274
181
  const advancedSource =
@@ -282,16 +189,23 @@ function normalizeRouteValidatorDefinition(sourceDefinition, { context = "route
282
189
  throw new RouteDefinitionError(`${context}.advanced must be an object.`);
283
190
  }
284
191
 
192
+ const unsupportedAdvancedKeys = Object.keys(advancedSource).filter(
193
+ (key) => !ADVANCED_VALIDATOR_OPTION_KEYS.includes(key)
194
+ );
195
+ if (unsupportedAdvancedKeys.length > 0) {
196
+ throw new RouteDefinitionError(`${context}.advanced.${unsupportedAdvancedKeys[0]} is not supported.`);
197
+ }
198
+
285
199
  const normalized = {
286
200
  meta,
287
- bodyValidator,
288
- queryValidator,
289
- paramsValidator
201
+ body,
202
+ query,
203
+ params
290
204
  };
291
205
 
292
- if (Object.prototype.hasOwnProperty.call(definition, "responseValidators")) {
293
- normalized.responseValidators = normalizeResponseValidatorDefinition(definition.responseValidators, {
294
- context: `${context}.responseValidators`
206
+ if (Object.prototype.hasOwnProperty.call(definition, "responses")) {
207
+ normalized.responses = normalizeResponseDefinition(definition.responses, {
208
+ context: `${context}.responses`
295
209
  });
296
210
  }
297
211
 
@@ -302,13 +216,6 @@ function normalizeRouteValidatorDefinition(sourceDefinition, { context = "route
302
216
  normalized.fastifySchema = fastifySchema;
303
217
  }
304
218
 
305
- const jskitInput = normalizeAdvancedJskitInput(advancedSource, {
306
- context
307
- });
308
- if (jskitInput) {
309
- normalized.jskitInput = jskitInput;
310
- }
311
-
312
219
  return Object.freeze(normalized);
313
220
  }
314
221
 
@@ -316,6 +223,14 @@ function compileNormalizedRouteValidator(normalizedValidator) {
316
223
  const schema = {};
317
224
  const input = {};
318
225
 
226
+ function createJsonRestSchemaInputTransform(definition, { defaultMode = "patch", context = "route validator" } = {}) {
227
+ return (payload) => validateSchemaPayload(definition, payload, {
228
+ phase: defaultMode === "replace" ? "output" : "input",
229
+ context,
230
+ statusCode: 400
231
+ });
232
+ }
233
+
319
234
  if (Array.isArray(normalizedValidator.meta?.tags) && normalizedValidator.meta.tags.length > 0) {
320
235
  schema.tags = [...normalizedValidator.meta.tags];
321
236
  }
@@ -323,32 +238,50 @@ function compileNormalizedRouteValidator(normalizedValidator) {
323
238
  schema.summary = normalizedValidator.meta.summary;
324
239
  }
325
240
 
326
- if (Object.prototype.hasOwnProperty.call(normalizedValidator.bodyValidator, "schema")) {
327
- schema.body = normalizedValidator.bodyValidator.schema;
328
- input.body = typeof normalizedValidator.bodyValidator.normalize === "function"
329
- ? normalizedValidator.bodyValidator.normalize
330
- : passThroughInputSection;
241
+ if (normalizedValidator.body) {
242
+ schema.body = resolveSchemaTransportSchemaDefinition(normalizedValidator.body, {
243
+ defaultMode: "patch",
244
+ context: "route validator.body"
245
+ });
246
+ input.body = createJsonRestSchemaInputTransform(normalizedValidator.body, {
247
+ defaultMode: "patch",
248
+ context: "route validator.body"
249
+ });
331
250
  }
332
251
 
333
- if (Object.prototype.hasOwnProperty.call(normalizedValidator.queryValidator, "schema")) {
334
- schema.querystring = normalizedValidator.queryValidator.schema;
335
- input.query = typeof normalizedValidator.queryValidator.normalize === "function"
336
- ? normalizedValidator.queryValidator.normalize
337
- : passThroughInputSection;
252
+ if (normalizedValidator.query) {
253
+ schema.querystring = resolveSchemaTransportSchemaDefinition(normalizedValidator.query, {
254
+ defaultMode: "patch",
255
+ context: "route validator.query"
256
+ });
257
+ input.query = createJsonRestSchemaInputTransform(normalizedValidator.query, {
258
+ defaultMode: "patch",
259
+ context: "route validator.query"
260
+ });
338
261
  }
339
262
 
340
- if (Object.prototype.hasOwnProperty.call(normalizedValidator.paramsValidator, "schema")) {
341
- schema.params = normalizedValidator.paramsValidator.schema;
342
- input.params = typeof normalizedValidator.paramsValidator.normalize === "function"
343
- ? normalizedValidator.paramsValidator.normalize
344
- : passThroughInputSection;
263
+ if (normalizedValidator.params) {
264
+ schema.params = resolveSchemaTransportSchemaDefinition(normalizedValidator.params, {
265
+ defaultMode: "patch",
266
+ context: "route validator.params"
267
+ });
268
+ input.params = createJsonRestSchemaInputTransform(normalizedValidator.params, {
269
+ defaultMode: "patch",
270
+ context: "route validator.params"
271
+ });
345
272
  }
346
273
 
347
- if (Object.prototype.hasOwnProperty.call(normalizedValidator, "responseValidators")) {
274
+ if (Object.prototype.hasOwnProperty.call(normalizedValidator, "responses")) {
348
275
  const responseSchema = {};
349
276
 
350
- for (const [statusCode, entry] of Object.entries(normalizedValidator.responseValidators || {})) {
351
- responseSchema[statusCode] = entry.schema;
277
+ for (const [statusCode, entry] of Object.entries(normalizedValidator.responses || {})) {
278
+ responseSchema[statusCode] =
279
+ entry && typeof entry === "object" && !Array.isArray(entry) && Object.prototype.hasOwnProperty.call(entry, "transportSchema")
280
+ ? entry.transportSchema
281
+ : resolveSchemaTransportSchemaDefinition(entry, {
282
+ defaultMode: "replace",
283
+ context: `route validator.responses.${statusCode}`
284
+ });
352
285
  }
353
286
 
354
287
  schema.response = responseSchema;
@@ -358,14 +291,10 @@ function compileNormalizedRouteValidator(normalizedValidator) {
358
291
  Object.assign(schema, normalizedValidator.fastifySchema);
359
292
  }
360
293
 
361
- if (normalizedValidator.jskitInput) {
362
- Object.assign(input, normalizedValidator.jskitInput);
363
- }
364
-
365
294
  const compiled = {};
366
295
  if (Object.keys(schema).length > 0) {
367
296
  compiled.schema = Object.freeze({
368
- ...schema
297
+ ...stripJsonRestTransportExtensions(schema)
369
298
  });
370
299
  }
371
300
  if (Object.keys(input).length > 0) {
@@ -433,14 +362,20 @@ function resolveRouteValidatorOptions({
433
362
  path
434
363
  });
435
364
 
365
+ const unsupportedRouteKeys = UNSUPPORTED_ROUTE_VALIDATOR_KEYS.filter((key) =>
366
+ Object.prototype.hasOwnProperty.call(normalizedOptions, key)
367
+ );
368
+ if (unsupportedRouteKeys.length > 0) {
369
+ throw new RouteDefinitionError(
370
+ `Route ${routeLabel} uses unsupported validator options: ${unsupportedRouteKeys.join(", ")}.`
371
+ );
372
+ }
373
+
436
374
  const hasInlineValidatorShape = VALIDATOR_OPTION_KEYS.some((key) => Object.prototype.hasOwnProperty.call(normalizedOptions, key));
437
375
 
438
376
  const remainingOptions = {
439
377
  ...normalizedOptions
440
378
  };
441
- delete remainingOptions.schema;
442
- delete remainingOptions.input;
443
- delete remainingOptions.validator;
444
379
 
445
380
  if (!hasInlineValidatorShape) {
446
381
  return remainingOptions;