@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,22 +1,38 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { Type } from "typebox";
3
+ import { createSchema } from "json-rest-schema";
4
4
 
5
5
  import { ModuleConfigError, defineModuleConfig } from "./moduleConfig.js";
6
6
 
7
7
  test("defineModuleConfig resolves valid config and freezes nested objects", () => {
8
8
  const moduleConfig = defineModuleConfig({
9
9
  moduleId: "contacts",
10
- schema: Type.Object(
11
- {
12
- mode: Type.Union([Type.Literal("standard"), Type.Literal("strict")]),
13
- maxContacts: Type.Integer({ minimum: 1 }),
14
- limits: Type.Object({
15
- inviteExpiryHours: Type.Integer({ minimum: 1, maximum: 168 })
16
- })
10
+ schema: createSchema({
11
+ mode: {
12
+ type: "string",
13
+ required: true,
14
+ enum: ["standard", "strict"]
17
15
  },
18
- { additionalProperties: false }
19
- ),
16
+ maxContacts: {
17
+ type: "integer",
18
+ required: true,
19
+ min: 1
20
+ },
21
+ limits: {
22
+ type: "object",
23
+ required: true,
24
+ validator(value) {
25
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
26
+ return "limits must be an object";
27
+ }
28
+ const inviteExpiryHours = Number(value.inviteExpiryHours);
29
+ if (!Number.isInteger(inviteExpiryHours) || inviteExpiryHours < 1 || inviteExpiryHours > 168) {
30
+ return "inviteExpiryHours must be an integer between 1 and 168";
31
+ }
32
+ return undefined;
33
+ }
34
+ }
35
+ }),
20
36
  load({ env }) {
21
37
  return {
22
38
  mode: String(env.CONTACTS_MODE || "standard"),
@@ -46,12 +62,13 @@ test("defineModuleConfig resolves valid config and freezes nested objects", () =
46
62
  test("defineModuleConfig reports schema validation issues with module-scoped details", () => {
47
63
  const moduleConfig = defineModuleConfig({
48
64
  moduleId: "contacts",
49
- schema: Type.Object(
50
- {
51
- maxContacts: Type.Integer({ minimum: 1 })
52
- },
53
- { additionalProperties: false }
54
- )
65
+ schema: createSchema({
66
+ maxContacts: {
67
+ type: "integer",
68
+ required: true,
69
+ min: 1
70
+ }
71
+ })
55
72
  });
56
73
 
57
74
  assert.throws(
@@ -66,13 +83,13 @@ test("defineModuleConfig reports schema validation issues with module-scoped det
66
83
  );
67
84
  });
68
85
 
69
- test("defineModuleConfig supports coercion via TypeBox Parse", () => {
86
+ test("defineModuleConfig supports coercion via json-rest-schema casts", () => {
70
87
  const moduleConfig = defineModuleConfig({
71
88
  moduleId: "contacts",
72
89
  coerce: true,
73
- schema: Type.Object({
74
- maxContacts: Type.Integer({ minimum: 1 }),
75
- enabled: Type.Boolean()
90
+ schema: createSchema({
91
+ maxContacts: { type: "integer", required: true, min: 1 },
92
+ enabled: { type: "boolean", required: true }
76
93
  })
77
94
  });
78
95
 
@@ -90,9 +107,16 @@ test("defineModuleConfig supports coercion via TypeBox Parse", () => {
90
107
  test("defineModuleConfig supports custom cross-field validate hook", () => {
91
108
  const moduleConfig = defineModuleConfig({
92
109
  moduleId: "contacts",
93
- schema: Type.Object({
94
- mode: Type.Union([Type.Literal("standard"), Type.Literal("strict")]),
95
- requireAuditTrail: Type.Boolean()
110
+ schema: createSchema({
111
+ mode: {
112
+ type: "string",
113
+ required: true,
114
+ enum: ["standard", "strict"]
115
+ },
116
+ requireAuditTrail: {
117
+ type: "boolean",
118
+ required: true
119
+ }
96
120
  }),
97
121
  validate(value) {
98
122
  if (value.mode === "strict" && value.requireAuditTrail !== true) {
@@ -133,7 +157,7 @@ test("defineModuleConfig rejects invalid module config definitions", () => {
133
157
  () =>
134
158
  defineModuleConfig({
135
159
  moduleId: "contacts",
136
- schema: Type.Object({}),
160
+ schema: createSchema({}),
137
161
  load: "not-a-function"
138
162
  }),
139
163
  /load/
@@ -580,12 +580,16 @@ function resolveInferredPageLinkTo({
580
580
  explicitLinkTo = "",
581
581
  pageTarget = {},
582
582
  parentHost = null,
583
- placementTarget = null
583
+ placementTarget = null,
584
+ suppressImplicitRelativeLinks = false
584
585
  } = {}) {
585
586
  const normalizedExplicitLinkTo = normalizeText(explicitLinkTo);
586
587
  if (normalizedExplicitLinkTo) {
587
588
  return normalizedExplicitLinkTo;
588
589
  }
590
+ if (suppressImplicitRelativeLinks === true) {
591
+ return "";
592
+ }
589
593
 
590
594
  const parentTargetId = normalizePlacementTargetId(parentHost);
591
595
  const placementTargetId = normalizePlacementTargetId(placementTarget);
@@ -670,24 +674,26 @@ async function resolvePageLinkTargetDetails({
670
674
  context,
671
675
  placement: normalizeText(placement) || parentHost?.id || ""
672
676
  });
677
+ const resolvedComponentToken = resolveInferredPageLinkComponentToken({
678
+ explicitComponentToken: componentToken,
679
+ parentHost,
680
+ placementTarget,
681
+ defaultComponentToken,
682
+ subpageComponentToken
683
+ });
673
684
 
674
685
  return Object.freeze({
675
686
  pageTarget: resolvedPageTarget,
676
687
  parentHost,
677
688
  placementTarget,
678
- componentToken: resolveInferredPageLinkComponentToken({
679
- explicitComponentToken: componentToken,
680
- parentHost,
681
- placementTarget,
682
- defaultComponentToken,
683
- subpageComponentToken
684
- }),
689
+ componentToken: resolvedComponentToken,
685
690
  whenLine: renderPageLinkWhenLine(resolvedPageTarget),
686
691
  linkTo: resolveInferredPageLinkTo({
687
692
  explicitLinkTo: linkTo,
688
693
  pageTarget: resolvedPageTarget,
689
694
  parentHost,
690
- placementTarget
695
+ placementTarget,
696
+ suppressImplicitRelativeLinks: resolvedComponentToken === (normalizeText(defaultComponentToken) || DEFAULT_PAGE_LINK_COMPONENT_TOKEN)
691
697
  })
692
698
  });
693
699
  }
@@ -316,7 +316,7 @@ test("resolvePageLinkTargetDetails prefers an outlet-declared default link token
316
316
  assert.equal(details.parentHost?.id, "home-settings:primary-menu");
317
317
  assert.equal(details.placementTarget.id, "home-settings:primary-menu");
318
318
  assert.equal(details.componentToken, "local.main.ui.surface-aware-menu-link-item");
319
- assert.equal(details.linkTo, "./pollen-types");
319
+ assert.equal(details.linkTo, "");
320
320
  });
321
321
  });
322
322
 
@@ -1,4 +1,4 @@
1
- import { Type } from "typebox";
1
+ import { createSchema } from "json-rest-schema";
2
2
  import { normalizeObject, normalizePositiveInteger as toPositiveInteger } from "../support/normalize.js";
3
3
  import { hasPermission } from "../support/permissions.js";
4
4
 
@@ -13,14 +13,9 @@ function resolveRequest(context) {
13
13
  return context?.requestMeta?.request || null;
14
14
  }
15
15
 
16
- const OBJECT_INPUT_VALIDATOR = Object.freeze({
17
- parse(value) {
18
- return normalizeObject(value);
19
- }
20
- });
21
-
22
- const EMPTY_INPUT_VALIDATOR = Object.freeze({
23
- schema: Type.Object({}, { additionalProperties: false })
16
+ const emptyInputValidator = Object.freeze({
17
+ schema: createSchema({}),
18
+ mode: "replace"
24
19
  });
25
20
 
26
21
  export {
@@ -29,6 +24,5 @@ export {
29
24
  requireServiceMethod,
30
25
  resolveRequest,
31
26
  hasPermission,
32
- EMPTY_INPUT_VALIDATOR,
33
- OBJECT_INPUT_VALIDATOR
27
+ emptyInputValidator
34
28
  };
@@ -1,6 +1,6 @@
1
- import { Type } from "typebox";
2
- import { mergeValidators } from "../validators/mergeValidators.js";
3
- import { normalizeObjectInput } from "../validators/inputNormalization.js";
1
+ import {
2
+ normalizeSingleSchemaDefinition
3
+ } from "../validators/index.js";
4
4
  import { isRecord as isPlainObject, normalizePositiveInteger } from "../support/normalize.js";
5
5
  import { normalizePermissionList } from "../support/permissions.js";
6
6
  import { normalizeText } from "./textNormalization.js";
@@ -76,7 +76,7 @@ function normalizeStringArray(value, { fieldName, allowedSet, allowEmpty = false
76
76
  return normalized;
77
77
  }
78
78
 
79
- function normalizeSingleActionValidator(value, fieldName, { required = false } = {}) {
79
+ function normalizeSingleActionSchema(value, fieldName, { required = false, defaultMode = "" } = {}) {
80
80
  if (value == null) {
81
81
  if (!required) {
82
82
  return null;
@@ -88,118 +88,47 @@ function normalizeSingleActionValidator(value, fieldName, { required = false } =
88
88
  }
89
89
 
90
90
  if (!isPlainObject(value)) {
91
- throw createActionRuntimeError(500, `Action definition ${fieldName} must be an object.`, {
91
+ throw createActionRuntimeError(500, `Action definition ${fieldName} must be a json-rest-schema schema definition.`, {
92
92
  code: "ACTION_DEFINITION_INVALID"
93
93
  });
94
94
  }
95
95
 
96
- if (!Object.prototype.hasOwnProperty.call(value, "schema")) {
97
- throw createActionRuntimeError(500, `Action definition ${fieldName}.schema is required.`, {
98
- code: "ACTION_DEFINITION_INVALID"
96
+ try {
97
+ return normalizeSingleSchemaDefinition(value, {
98
+ context: `Action definition ${fieldName}`,
99
+ defaultMode
99
100
  });
100
- }
101
-
102
- if (
103
- value.schema == null ||
104
- (typeof value.schema !== "function" && (typeof value.schema !== "object" || Array.isArray(value.schema)))
105
- ) {
106
- throw createActionRuntimeError(500, `Action definition ${fieldName}.schema must be a function or object.`, {
101
+ } catch (error) {
102
+ throw createActionRuntimeError(500, error?.message || `Action definition ${fieldName} is invalid.`, {
107
103
  code: "ACTION_DEFINITION_INVALID"
108
104
  });
109
105
  }
110
-
111
- if (Object.prototype.hasOwnProperty.call(value, "normalize")) {
112
- if (value.normalize != null && typeof value.normalize !== "function") {
113
- throw createActionRuntimeError(500, `Action definition ${fieldName}.normalize must be a function.`, {
114
- code: "ACTION_DEFINITION_INVALID"
115
- });
116
- }
117
- }
118
-
119
- return Object.freeze({
120
- schema: value.schema,
121
- ...(typeof value.normalize === "function" ? { normalize: value.normalize } : {})
122
- });
123
- }
124
-
125
- function isActionValidatorShape(value) {
126
- return (
127
- isPlainObject(value) &&
128
- (Object.prototype.hasOwnProperty.call(value, "schema") || Object.prototype.hasOwnProperty.call(value, "normalize"))
129
- );
130
106
  }
131
107
 
132
- function normalizeSectionActionValidatorMap(value, fieldName) {
133
- if (!isPlainObject(value) || isActionValidatorShape(value)) {
134
- return null;
135
- }
108
+ function normalizeActionInputDefinition(value, fieldName, { required = false } = {}) {
109
+ if (value == null) {
110
+ if (!required) {
111
+ return null;
112
+ }
136
113
 
137
- const entries = Object.entries(value);
138
- if (entries.length < 1) {
139
- throw createActionRuntimeError(500, `Action definition ${fieldName} must define at least one section validator.`, {
114
+ throw createActionRuntimeError(500, `Action definition ${fieldName} is required.`, {
140
115
  code: "ACTION_DEFINITION_INVALID"
141
116
  });
142
117
  }
143
118
 
144
- const schemaProperties = {};
145
- const sectionNormalizers = [];
146
-
147
- for (const [rawKey, rawValidator] of entries) {
148
- const sectionKey = normalizeText(rawKey);
149
- if (!sectionKey) {
150
- throw createActionRuntimeError(500, `Action definition ${fieldName} section keys must be non-empty strings.`, {
151
- code: "ACTION_DEFINITION_INVALID"
152
- });
153
- }
154
-
155
- const sectionValidator = normalizeSingleActionValidator(rawValidator, `${fieldName}.${sectionKey}`, {
156
- required: true
157
- });
158
-
159
- schemaProperties[sectionKey] = sectionValidator.schema;
160
- sectionNormalizers.push({
161
- key: sectionKey,
162
- normalize: typeof sectionValidator.normalize === "function" ? sectionValidator.normalize : null
119
+ if (Array.isArray(value)) {
120
+ throw createActionRuntimeError(500, `Action definition ${fieldName} must be a single schema definition.`, {
121
+ code: "ACTION_DEFINITION_INVALID"
163
122
  });
164
123
  }
165
124
 
166
- return Object.freeze({
167
- schema: Type.Object(schemaProperties, {
168
- additionalProperties: false
169
- }),
170
- async normalize(payload, meta) {
171
- const source = normalizeObjectInput(payload);
172
- const normalized = {};
173
-
174
- for (const section of sectionNormalizers) {
175
- if (!Object.hasOwn(source, section.key)) {
176
- continue;
177
- }
178
-
179
- const sectionPayload = source[section.key];
180
- normalized[section.key] = section.normalize ? await section.normalize(sectionPayload, meta) : sectionPayload;
181
- }
182
-
183
- return normalized;
184
- }
125
+ return normalizeSingleActionSchema(value, fieldName, {
126
+ required,
127
+ defaultMode: "patch"
185
128
  });
186
129
  }
187
130
 
188
- function mergeNormalizedActionValidators(validators, fieldName) {
189
- return mergeValidators(validators, {
190
- context: `Action definition ${fieldName}`,
191
- requireSchema: true,
192
- requiredSchemaMessage: `Action definition ${fieldName}.schema is required.`,
193
- normalizeResultMessage: `Action definition ${fieldName}.normalize must return an object.`,
194
- createError(message) {
195
- return createActionRuntimeError(500, message, {
196
- code: "ACTION_DEFINITION_INVALID"
197
- });
198
- }
199
- });
200
- }
201
-
202
- function normalizeActionValidators(value, fieldName, { required = false } = {}) {
131
+ function normalizeActionOutputDefinition(value, fieldName, { required = false } = {}) {
203
132
  if (value == null) {
204
133
  if (!required) {
205
134
  return null;
@@ -210,40 +139,15 @@ function normalizeActionValidators(value, fieldName, { required = false } = {})
210
139
  });
211
140
  }
212
141
 
213
- const validatorsSource = Array.isArray(value) ? value : [value];
214
-
215
- if (validatorsSource.length < 1) {
216
- throw createActionRuntimeError(500, `Action definition ${fieldName} is required.`, {
142
+ if (Array.isArray(value)) {
143
+ throw createActionRuntimeError(500, `Action definition ${fieldName} must be a single schema definition.`, {
217
144
  code: "ACTION_DEFINITION_INVALID"
218
145
  });
219
146
  }
220
147
 
221
- const validators = validatorsSource.map((entry, index) => {
222
- const contextFieldName = `${fieldName}[${index}]`;
223
- const sectionMapValidator = normalizeSectionActionValidatorMap(entry, contextFieldName);
224
- if (sectionMapValidator) {
225
- return sectionMapValidator;
226
- }
227
-
228
- const validator = normalizeSingleActionValidator(entry, contextFieldName, {
229
- required: true
230
- });
231
-
232
- if (!validator) {
233
- throw createActionRuntimeError(500, `Action definition ${contextFieldName} is required.`, {
234
- code: "ACTION_DEFINITION_INVALID"
235
- });
236
- }
237
-
238
- return validator;
239
- });
240
-
241
- return mergeNormalizedActionValidators(validators, fieldName);
242
- }
243
-
244
- function normalizeActionOutputValidator(value, fieldName, { required = false } = {}) {
245
- return normalizeActionValidators(value, fieldName, {
246
- required
148
+ return normalizeSingleActionSchema(value, fieldName, {
149
+ required,
150
+ defaultMode: "replace"
247
151
  });
248
152
  }
249
153
 
@@ -333,7 +237,9 @@ function normalizeActionExtensions(value) {
333
237
  });
334
238
  }
335
239
 
336
- return Object.freeze(normalizeObjectInput(value));
240
+ return Object.freeze({
241
+ ...value
242
+ });
337
243
  }
338
244
 
339
245
  function normalizeActionDefinition(definition, { contributorId = "", contributorDomain = "" } = {}) {
@@ -369,12 +275,6 @@ function normalizeActionDefinition(definition, { contributorId = "", contributor
369
275
  fieldName: "surfaces"
370
276
  });
371
277
 
372
- if (Object.prototype.hasOwnProperty.call(source, "visibility")) {
373
- throw createActionRuntimeError(500, `Action definition \"${id}\" visibility is not supported.`, {
374
- code: "ACTION_DEFINITION_INVALID"
375
- });
376
- }
377
-
378
278
  const idempotency = normalizeText(source.idempotency || "none").toLowerCase();
379
279
  if (!ACTION_IDEMPOTENCY_SET.has(idempotency)) {
380
280
  throw createActionRuntimeError(
@@ -392,18 +292,6 @@ function normalizeActionDefinition(definition, { contributorId = "", contributor
392
292
  });
393
293
  }
394
294
 
395
- if (Object.prototype.hasOwnProperty.call(source, "input")) {
396
- throw createActionRuntimeError(500, `Action definition \"${id}\" must use inputValidator instead of input.`, {
397
- code: "ACTION_DEFINITION_INVALID"
398
- });
399
- }
400
-
401
- if (Object.prototype.hasOwnProperty.call(source, "output")) {
402
- throw createActionRuntimeError(500, `Action definition \"${id}\" must use outputValidator instead of output.`, {
403
- code: "ACTION_DEFINITION_INVALID"
404
- });
405
- }
406
-
407
295
  return Object.freeze({
408
296
  id,
409
297
  version,
@@ -411,10 +299,10 @@ function normalizeActionDefinition(definition, { contributorId = "", contributor
411
299
  kind,
412
300
  channels,
413
301
  surfaces,
414
- inputValidator: normalizeActionValidators(source.inputValidator, "inputValidator", {
302
+ input: normalizeActionInputDefinition(source.input, "input", {
415
303
  required: true
416
304
  }),
417
- outputValidator: normalizeActionOutputValidator(source.outputValidator, "outputValidator", {
305
+ output: normalizeActionOutputDefinition(source.output, "output", {
418
306
  required: false
419
307
  }),
420
308
  idempotency,
@@ -466,10 +354,9 @@ const __testables = {
466
354
  normalizeText,
467
355
  isPlainObject,
468
356
  normalizeStringArray,
469
- normalizeSingleActionValidator,
470
- normalizeSectionActionValidatorMap,
471
- normalizeActionValidators,
472
- normalizeActionOutputValidator,
357
+ normalizeSingleActionSchema,
358
+ normalizeActionInputDefinition,
359
+ normalizeActionOutputDefinition,
473
360
  normalizeActionPermission,
474
361
  normalizeAuditConfig,
475
362
  normalizeObservabilityConfig,