@jskit-ai/kernel 0.1.55 → 0.1.57

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 (57) 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/normalize.js +7 -0
  31. package/shared/support/normalize.test.js +4 -2
  32. package/shared/support/shellLayoutTargets.test.js +1 -1
  33. package/shared/validators/composeSchemaDefinitions.js +53 -0
  34. package/shared/validators/composeSchemaDefinitions.test.js +156 -0
  35. package/shared/validators/createCursorListValidator.js +22 -35
  36. package/shared/validators/createCursorListValidator.test.js +22 -23
  37. package/shared/validators/cursorPaginationQueryValidator.js +14 -24
  38. package/shared/validators/cursorPaginationQueryValidator.test.js +18 -8
  39. package/shared/validators/htmlTimeSchemas.js +6 -4
  40. package/shared/validators/index.js +15 -7
  41. package/shared/validators/jsonRestSchemaSupport.js +139 -0
  42. package/shared/validators/mergeObjectSchemas.js +44 -6
  43. package/shared/validators/mergeObjectSchemas.test.js +60 -35
  44. package/shared/validators/recordIdParamsValidator.js +19 -52
  45. package/shared/validators/recordIdParamsValidator.test.js +13 -8
  46. package/shared/validators/resourceRequiredMetadata.js +3 -3
  47. package/shared/validators/resourceRequiredMetadata.test.js +29 -16
  48. package/shared/validators/schemaDefinitions.js +126 -0
  49. package/shared/validators/schemaDefinitions.test.js +51 -0
  50. package/shared/validators/schemaPayloadValidation.js +65 -0
  51. package/test/barrelExposure.test.js +30 -0
  52. package/test/routeInputContractGuard.test.js +10 -6
  53. package/shared/validators/mergeValidators.js +0 -89
  54. package/shared/validators/mergeValidators.test.js +0 -116
  55. package/shared/validators/nestValidator.js +0 -53
  56. package/shared/validators/nestValidator.test.js +0 -60
  57. package/shared/validators/settingsFieldNormalization.js +0 -40
@@ -1,22 +1,29 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { Type } from "typebox";
4
3
  import { mergeObjectSchemas } from "./mergeObjectSchemas.js";
5
4
 
6
5
  test("mergeObjectSchemas merges disjoint object schemas", () => {
7
6
  const mergedSchema = mergeObjectSchemas([
8
- Type.Object(
9
- {
10
- cursor: Type.Optional(Type.String({ minLength: 1 }))
11
- },
12
- { additionalProperties: false }
13
- ),
14
- Type.Object(
15
- {
16
- search: Type.Optional(Type.String({ minLength: 1 }))
17
- },
18
- { additionalProperties: false }
19
- )
7
+ {
8
+ type: "object",
9
+ additionalProperties: false,
10
+ properties: {
11
+ cursor: {
12
+ type: "string",
13
+ minLength: 1
14
+ }
15
+ }
16
+ },
17
+ {
18
+ type: "object",
19
+ additionalProperties: false,
20
+ properties: {
21
+ search: {
22
+ type: "string",
23
+ minLength: 1
24
+ }
25
+ }
26
+ }
20
27
  ]);
21
28
 
22
29
  assert.equal(mergedSchema.type, "object");
@@ -28,18 +35,28 @@ test("mergeObjectSchemas merges disjoint object schemas", () => {
28
35
 
29
36
  test("mergeObjectSchemas preserves required fields through merged property definitions", () => {
30
37
  const mergedSchema = mergeObjectSchemas([
31
- Type.Object(
32
- {
33
- workspaceSlug: Type.String({ minLength: 1 })
38
+ {
39
+ type: "object",
40
+ additionalProperties: false,
41
+ properties: {
42
+ workspaceSlug: {
43
+ type: "string",
44
+ minLength: 1
45
+ }
34
46
  },
35
- { additionalProperties: false }
36
- ),
37
- Type.Object(
38
- {
39
- inviteId: Type.String({ minLength: 1 })
47
+ required: ["workspaceSlug"]
48
+ },
49
+ {
50
+ type: "object",
51
+ additionalProperties: false,
52
+ properties: {
53
+ inviteId: {
54
+ type: "string",
55
+ minLength: 1
56
+ }
40
57
  },
41
- { additionalProperties: false }
42
- )
58
+ required: ["inviteId"]
59
+ }
43
60
  ]);
44
61
 
45
62
  assert.deepEqual(mergedSchema.required, ["workspaceSlug", "inviteId"]);
@@ -49,18 +66,26 @@ test("mergeObjectSchemas rejects duplicate properties with different schema obje
49
66
  assert.throws(
50
67
  () =>
51
68
  mergeObjectSchemas([
52
- Type.Object(
53
- {
54
- workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
55
- },
56
- { additionalProperties: false }
57
- ),
58
- Type.Object(
59
- {
60
- workspaceSlug: Type.Optional(Type.String({ minLength: 2 }))
61
- },
62
- { additionalProperties: false }
63
- )
69
+ {
70
+ type: "object",
71
+ additionalProperties: false,
72
+ properties: {
73
+ workspaceSlug: {
74
+ type: "string",
75
+ minLength: 1
76
+ }
77
+ }
78
+ },
79
+ {
80
+ type: "object",
81
+ additionalProperties: false,
82
+ properties: {
83
+ workspaceSlug: {
84
+ type: "string",
85
+ minLength: 2
86
+ }
87
+ }
88
+ }
64
89
  ]),
65
90
  /duplicate property "workspaceSlug"/
66
91
  );
@@ -1,64 +1,34 @@
1
- import { Type } from "typebox";
2
- import { normalizeObjectInput } from "./inputNormalization.js";
3
- import { normalizePositiveInteger, normalizeRecordId } from "../support/normalize.js";
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "../support/deepFreeze.js";
4
3
 
5
4
  const RECORD_ID_PATTERN = "^[1-9][0-9]*$";
6
5
 
7
- const recordIdSchema = Type.String({
6
+ const recordIdSchema = deepFreeze({
7
+ type: "string",
8
8
  minLength: 1,
9
9
  pattern: RECORD_ID_PATTERN
10
10
  });
11
11
 
12
12
  const recordIdInputSchema = recordIdSchema;
13
13
 
14
- const nullableRecordIdSchema = Type.Union([recordIdSchema, Type.Null()]);
15
- const nullableRecordIdInputSchema = Type.Union([recordIdInputSchema, Type.Null()]);
16
-
17
- const positiveIntegerValidator = Object.freeze({
18
- schema: Type.Union([
19
- Type.Integer({ minimum: 1 }),
20
- Type.String({ minLength: 1, pattern: RECORD_ID_PATTERN })
21
- ]),
22
- normalize(value) {
23
- return normalizePositiveInteger(value);
24
- }
25
- });
26
-
27
- const recordIdValidator = Object.freeze({
28
- schema: recordIdInputSchema,
29
- normalize(value) {
30
- return normalizeRecordId(value, {
31
- fallback: ""
32
- });
33
- }
14
+ const nullableRecordIdSchema = deepFreeze({
15
+ ...recordIdSchema,
16
+ nullable: true
34
17
  });
35
18
 
36
- const nullableRecordIdValidator = Object.freeze({
37
- schema: nullableRecordIdInputSchema,
38
- normalize(value) {
39
- return normalizeRecordId(value, {
40
- fallback: null
41
- });
42
- }
43
- });
44
-
45
- const recordIdParamsValidator = Object.freeze({
46
- schema: Type.Object(
47
- {
48
- recordId: Type.Optional(recordIdInputSchema)
49
- },
50
- { additionalProperties: false }
51
- ),
52
- normalize(input = {}) {
53
- const source = normalizeObjectInput(input);
54
- const normalized = {};
19
+ const nullableRecordIdInputSchema = nullableRecordIdSchema;
55
20
 
56
- if (Object.hasOwn(source, "recordId")) {
57
- normalized.recordId = recordIdValidator.normalize(source.recordId);
21
+ const recordIdParamsValidator = deepFreeze({
22
+ schema: createSchema({
23
+ recordId: {
24
+ ...recordIdSchema,
25
+ required: true,
26
+ messages: {
27
+ pattern: "Record id must be a canonical positive integer string."
28
+ }
58
29
  }
59
-
60
- return normalized;
61
- }
30
+ }),
31
+ mode: "patch"
62
32
  });
63
33
 
64
34
  export {
@@ -67,8 +37,5 @@ export {
67
37
  recordIdInputSchema,
68
38
  nullableRecordIdSchema,
69
39
  nullableRecordIdInputSchema,
70
- recordIdValidator,
71
- nullableRecordIdValidator,
72
- recordIdParamsValidator,
73
- positiveIntegerValidator
40
+ recordIdParamsValidator
74
41
  };
@@ -4,20 +4,25 @@ import assert from "node:assert/strict";
4
4
  import { recordIdParamsValidator } from "./recordIdParamsValidator.js";
5
5
 
6
6
  test("recordIdParamsValidator normalizes canonical string ids", () => {
7
- assert.deepEqual(recordIdParamsValidator.normalize({ recordId: "42" }), {
7
+ const { validatedObject, errors } = recordIdParamsValidator.schema.patch({ recordId: "42" });
8
+ assert.deepEqual(errors, {});
9
+ assert.deepEqual(validatedObject, {
8
10
  recordId: "42"
9
11
  });
10
12
  });
11
13
 
12
14
  test("recordIdParamsValidator rejects invalid ids", () => {
13
- assert.deepEqual(recordIdParamsValidator.normalize({ recordId: "nope" }), {
14
- recordId: ""
15
- });
16
- assert.deepEqual(recordIdParamsValidator.normalize({ recordId: 42 }), {
17
- recordId: ""
18
- });
15
+ const invalidString = recordIdParamsValidator.schema.patch({ recordId: "nope" });
16
+ assert.equal(invalidString.validatedObject.recordId, "nope");
17
+ assert.equal(invalidString.errors.recordId?.code, "PATTERN");
18
+
19
+ const numericId = recordIdParamsValidator.schema.patch({ recordId: 42 });
20
+ assert.equal(numericId.validatedObject.recordId, "42");
21
+ assert.deepEqual(numericId.errors, {});
19
22
  });
20
23
 
21
24
  test("recordIdParamsValidator keeps absent key absent", () => {
22
- assert.deepEqual(recordIdParamsValidator.normalize({}), {});
25
+ const { validatedObject, errors } = recordIdParamsValidator.schema.patch({});
26
+ assert.deepEqual(errors, {});
27
+ assert.deepEqual(validatedObject, {});
23
28
  });
@@ -23,9 +23,9 @@ function deriveResourceRequiredMetadata(resourceSchema) {
23
23
  ? resourceSchema.operations
24
24
  : null;
25
25
 
26
- const createSchema = operations?.create?.bodyValidator?.schema;
27
- const replaceSchema = operations?.replace?.bodyValidator?.schema;
28
- const patchSchema = operations?.patch?.bodyValidator?.schema;
26
+ const createSchema = operations?.create?.body?.schema;
27
+ const replaceSchema = operations?.replace?.body?.schema;
28
+ const patchSchema = operations?.patch?.body?.schema;
29
29
 
30
30
  return Object.freeze({
31
31
  create: deriveRequiredFieldsFromSchema(createSchema),
@@ -1,6 +1,5 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { Type } from "typebox";
4
3
  import {
5
4
  normalizeRequiredFieldList,
6
5
  deriveRequiredFieldsFromSchema,
@@ -16,28 +15,42 @@ test("normalizeRequiredFieldList trims, dedupes, and drops empty entries", () =>
16
15
  });
17
16
 
18
17
  test("deriveRequiredFieldsFromSchema reads schema.required", () => {
19
- const schema = Type.Object({
20
- name: Type.String(),
21
- color: Type.String(),
22
- optionalField: Type.Optional(Type.String())
23
- });
18
+ const schema = {
19
+ type: "object",
20
+ properties: {
21
+ name: { type: "string" },
22
+ color: { type: "string" },
23
+ optionalField: { type: "string" }
24
+ },
25
+ required: ["name", "color"]
26
+ };
24
27
 
25
28
  assert.deepEqual(deriveRequiredFieldsFromSchema(schema), ["name", "color"]);
26
- assert.deepEqual(deriveRequiredFieldsFromSchema(Type.Partial(schema)), []);
29
+ assert.deepEqual(deriveRequiredFieldsFromSchema({
30
+ ...schema,
31
+ required: []
32
+ }), []);
27
33
  });
28
34
 
29
35
  test("deriveResourceRequiredMetadata reads create/replace/patch operation body schemas", () => {
30
- const fullSchema = Type.Object({
31
- name: Type.String(),
32
- color: Type.String(),
33
- invitesEnabled: Type.Boolean()
34
- });
35
- const patchSchema = Type.Partial(fullSchema);
36
+ const fullSchema = {
37
+ type: "object",
38
+ properties: {
39
+ name: { type: "string" },
40
+ color: { type: "string" },
41
+ invitesEnabled: { type: "boolean" }
42
+ },
43
+ required: ["name", "color", "invitesEnabled"]
44
+ };
45
+ const patchSchema = {
46
+ ...fullSchema,
47
+ required: []
48
+ };
36
49
  const resource = {
37
50
  operations: {
38
- create: { bodyValidator: { schema: fullSchema } },
39
- replace: { bodyValidator: { schema: fullSchema } },
40
- patch: { bodyValidator: { schema: patchSchema } }
51
+ create: { body: { schema: fullSchema } },
52
+ replace: { body: { schema: fullSchema } },
53
+ patch: { body: { schema: patchSchema } }
41
54
  }
42
55
  };
43
56
 
@@ -0,0 +1,126 @@
1
+ import { normalizeObject, normalizeText } from "../support/normalize.js";
2
+ import {
3
+ executeSchemaDefinition,
4
+ isJsonRestSchemaInstance,
5
+ normalizeJsonRestSchemaFieldErrors,
6
+ resolveSchemaDefinitionMode,
7
+ resolveSchemaDefinitionTransportSchema
8
+ } from "./jsonRestSchemaSupport.js";
9
+
10
+ function isSchemaDefinitionObject(value) {
11
+ return Boolean(value) &&
12
+ typeof value === "object" &&
13
+ !Array.isArray(value) &&
14
+ (
15
+ Object.prototype.hasOwnProperty.call(value, "schema") ||
16
+ Object.prototype.hasOwnProperty.call(value, "mode")
17
+ );
18
+ }
19
+
20
+ function normalizeSingleSchemaDefinition(value, { context = "schema definition", defaultMode = "" } = {}) {
21
+ if (value == null) {
22
+ return null;
23
+ }
24
+
25
+ if (!isSchemaDefinitionObject(value)) {
26
+ throw new TypeError(`${context} must be a schema definition object.`);
27
+ }
28
+
29
+ const source = normalizeObject(value);
30
+
31
+ if (!Object.prototype.hasOwnProperty.call(source, "schema")) {
32
+ throw new TypeError(`${context}.schema is required.`);
33
+ }
34
+
35
+ if (!isJsonRestSchemaInstance(source.schema)) {
36
+ throw new TypeError(`${context}.schema must be a json-rest-schema schema instance.`);
37
+ }
38
+
39
+ const normalized = {
40
+ schema: source.schema
41
+ };
42
+
43
+ const resolvedDefaultMode = normalizeText(defaultMode).toLowerCase();
44
+ normalized.mode = resolveSchemaDefinitionMode(source, {
45
+ defaultMode: resolvedDefaultMode || "patch",
46
+ context: `${context}.mode`
47
+ });
48
+
49
+ return Object.freeze(normalized);
50
+ }
51
+
52
+ function normalizeSchemaDefinition(value, {
53
+ context = "schema definition",
54
+ defaultMode = ""
55
+ } = {}) {
56
+ return normalizeSingleSchemaDefinition(value, {
57
+ context,
58
+ defaultMode
59
+ });
60
+ }
61
+
62
+ function resolveSchemaTransportSchemaDefinition(value, {
63
+ context = "schema definition",
64
+ defaultMode = ""
65
+ } = {}) {
66
+ const normalized = normalizeSchemaDefinition(value, {
67
+ context,
68
+ defaultMode
69
+ });
70
+
71
+ if (!normalized) {
72
+ return undefined;
73
+ }
74
+
75
+ return resolveSchemaDefinitionTransportSchema(normalized, {
76
+ defaultMode: defaultMode || "patch",
77
+ context: `${context}.mode`
78
+ });
79
+ }
80
+
81
+ function resolveStructuredSchemaTransportSchema(value, {
82
+ context = "schema definition",
83
+ defaultMode = ""
84
+ } = {}) {
85
+ return resolveSchemaTransportSchemaDefinition(value, {
86
+ context,
87
+ defaultMode
88
+ });
89
+ }
90
+
91
+ function hasJsonRestSchemaDefinition(value) {
92
+ try {
93
+ return Boolean(normalizeSchemaDefinition(value));
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function executeJsonRestSchemaDefinition(value, payload, {
100
+ context = "schema definition",
101
+ defaultMode = ""
102
+ } = {}) {
103
+ const normalized = normalizeSchemaDefinition(value, {
104
+ context,
105
+ defaultMode
106
+ });
107
+
108
+ if (!normalized) {
109
+ return null;
110
+ }
111
+
112
+ return executeSchemaDefinition(normalized, payload, {
113
+ defaultMode: defaultMode || "patch",
114
+ context: `${context}.mode`
115
+ });
116
+ }
117
+
118
+ export {
119
+ hasJsonRestSchemaDefinition,
120
+ normalizeSingleSchemaDefinition,
121
+ normalizeSchemaDefinition,
122
+ resolveSchemaTransportSchemaDefinition,
123
+ resolveStructuredSchemaTransportSchema,
124
+ executeJsonRestSchemaDefinition,
125
+ normalizeJsonRestSchemaFieldErrors
126
+ };
@@ -0,0 +1,51 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createSchema } from "json-rest-schema";
4
+ import {
5
+ normalizeSingleSchemaDefinition,
6
+ resolveSchemaTransportSchemaDefinition
7
+ } from "./schemaDefinitions.js";
8
+
9
+ test("normalizeSingleSchemaDefinition validates mode eagerly", () => {
10
+ const definition = {
11
+ schema: createSchema({
12
+ name: { type: "string", required: true, minLength: 1 }
13
+ }),
14
+ mode: "wrong"
15
+ };
16
+
17
+ assert.throws(
18
+ () => normalizeSingleSchemaDefinition(definition, {
19
+ context: "test.definition"
20
+ }),
21
+ /test\.definition\.mode must be one of: create, replace, patch\./
22
+ );
23
+ });
24
+
25
+ test("normalizeSingleSchemaDefinition preserves valid normalized mode", () => {
26
+ const definition = normalizeSingleSchemaDefinition({
27
+ schema: createSchema({
28
+ name: { type: "string", required: true, minLength: 1 }
29
+ }),
30
+ mode: " Replace "
31
+ }, {
32
+ context: "test.definition"
33
+ });
34
+
35
+ assert.equal(definition.mode, "replace");
36
+ });
37
+
38
+ test("resolveSchemaTransportSchemaDefinition still resolves valid definitions", () => {
39
+ const transportSchema = resolveSchemaTransportSchemaDefinition({
40
+ schema: createSchema({
41
+ name: { type: "string", required: true, minLength: 1 }
42
+ }),
43
+ mode: "patch"
44
+ }, {
45
+ context: "test.definition"
46
+ });
47
+
48
+ assert.equal(transportSchema.type, "object");
49
+ assert.equal(transportSchema.additionalProperties, false);
50
+ assert.equal(transportSchema.properties.name.type, "string");
51
+ });
@@ -0,0 +1,65 @@
1
+ import {
2
+ executeJsonRestSchemaDefinition,
3
+ normalizeJsonRestSchemaFieldErrors
4
+ } from "./schemaDefinitions.js";
5
+
6
+ function buildSchemaValidationError({
7
+ message = "Schema validation failed.",
8
+ fieldErrors = null,
9
+ errors = null,
10
+ cause,
11
+ statusCode = null
12
+ } = {}) {
13
+ const error = new Error(message, cause ? { cause } : undefined);
14
+ if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599) {
15
+ error.statusCode = statusCode;
16
+ }
17
+ if (fieldErrors && typeof fieldErrors === "object") {
18
+ error.fieldErrors = fieldErrors;
19
+ error.details = {
20
+ ...(error.details || {}),
21
+ fieldErrors
22
+ };
23
+ } else if (errors !== null && errors !== undefined) {
24
+ error.details = {
25
+ ...(error.details || {}),
26
+ errors
27
+ };
28
+ }
29
+
30
+ return error;
31
+ }
32
+
33
+ function validateSchemaPayload(schemaDefinition, payload, {
34
+ phase = "input",
35
+ context = "schema definition",
36
+ statusCode = null
37
+ } = {}) {
38
+ if (schemaDefinition == null) {
39
+ return payload;
40
+ }
41
+
42
+ const result = executeJsonRestSchemaDefinition(schemaDefinition, payload, {
43
+ defaultMode: phase === "output" ? "replace" : "patch",
44
+ context
45
+ });
46
+
47
+ if (!result) {
48
+ throw new TypeError(`${context}.schema must be a json-rest-schema schema instance.`);
49
+ }
50
+
51
+ const fieldErrors = normalizeJsonRestSchemaFieldErrors(result?.errors, schemaDefinition);
52
+ if (Object.keys(fieldErrors).length > 0) {
53
+ throw buildSchemaValidationError({
54
+ fieldErrors,
55
+ statusCode
56
+ });
57
+ }
58
+
59
+ return result?.validatedObject ?? payload;
60
+ }
61
+
62
+ export {
63
+ buildSchemaValidationError,
64
+ validateSchemaPayload
65
+ };
@@ -47,6 +47,36 @@ const BARREL_EXPECTATIONS = Object.freeze([
47
47
  "withActionDefaults"
48
48
  ])
49
49
  }),
50
+ Object.freeze({
51
+ filePath: path.join(REPO_ROOT, "packages", "kernel", "shared", "validators", "index.js"),
52
+ expectedExports: Object.freeze([
53
+ "HTML_TIME_STRING_SCHEMA",
54
+ "NULLABLE_HTML_TIME_STRING_SCHEMA",
55
+ "RECORD_ID_PATTERN",
56
+ "buildSchemaValidationError",
57
+ "composeSchemaDefinitions",
58
+ "createCursorListValidator",
59
+ "createSchema",
60
+ "cursorPaginationQueryValidator",
61
+ "deriveRequiredFieldsFromSchema",
62
+ "deriveResourceRequiredMetadata",
63
+ "executeJsonRestSchemaDefinition",
64
+ "hasJsonRestSchemaDefinition",
65
+ "mergeObjectSchemas",
66
+ "normalizeObjectInput",
67
+ "normalizeRequiredFieldList",
68
+ "normalizeSchemaDefinition",
69
+ "normalizeSingleSchemaDefinition",
70
+ "nullableRecordIdInputSchema",
71
+ "nullableRecordIdSchema",
72
+ "recordIdInputSchema",
73
+ "recordIdParamsValidator",
74
+ "recordIdSchema",
75
+ "resolveSchemaTransportSchemaDefinition",
76
+ "resolveStructuredSchemaTransportSchema",
77
+ "validateSchemaPayload"
78
+ ])
79
+ }),
50
80
  Object.freeze({
51
81
  filePath: path.join(REPO_ROOT, "packages", "kernel", "client", "index.js"),
52
82
  expectedExports: Object.freeze([
@@ -13,10 +13,6 @@ const DISALLOWED_PATTERNS = [
13
13
  {
14
14
  description: "spread pass-through from request.input",
15
15
  regex: /\.{3}\s*request\.input\.(body|query|params)\b/g
16
- },
17
- {
18
- description: "whole-section pass-through from request.input",
19
- regex: /\binput\s*:\s*request\.input\.(body|query|params)\b/g
20
16
  }
21
17
  ];
22
18
 
@@ -50,13 +46,21 @@ function findLineNumber(sourceText, index) {
50
46
  return sourceText.slice(0, index).split("\n").length;
51
47
  }
52
48
 
53
- test("server route handlers do not pass through request.input sections", async () => {
49
+ test("server route handlers do not spread request.input sections into ad hoc shapes", async () => {
54
50
  const allFiles = await listFilesRecursive(PACKAGES_ROOT);
55
51
  const targetFiles = allFiles.filter((absolutePath) => isRouteServerSourceFile(absolutePath));
56
52
  const violations = [];
57
53
 
58
54
  for (const absolutePath of targetFiles) {
59
- const sourceText = await readFile(absolutePath, "utf8");
55
+ let sourceText = "";
56
+ try {
57
+ sourceText = await readFile(absolutePath, "utf8");
58
+ } catch (error) {
59
+ if (error?.code === "ENOENT") {
60
+ continue;
61
+ }
62
+ throw error;
63
+ }
60
64
  for (const { regex, description } of DISALLOWED_PATTERNS) {
61
65
  regex.lastIndex = 0;
62
66
  let match = regex.exec(sourceText);