@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
@@ -0,0 +1,156 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createSchema } from "json-rest-schema";
4
+ import { composeSchemaDefinitions } from "./composeSchemaDefinitions.js";
5
+
6
+ test("composeSchemaDefinitions merges multiple schema definitions into one contract", () => {
7
+ const params = {
8
+ schema: createSchema({
9
+ workspaceSlug: {
10
+ type: "string",
11
+ required: true,
12
+ minLength: 1
13
+ }
14
+ }),
15
+ mode: "patch"
16
+ };
17
+ const query = {
18
+ schema: createSchema({
19
+ q: {
20
+ type: "string",
21
+ required: false,
22
+ minLength: 1
23
+ }
24
+ }),
25
+ mode: "patch"
26
+ };
27
+
28
+ const definition = composeSchemaDefinitions([params, query], {
29
+ context: "test.compose"
30
+ });
31
+
32
+ assert.equal(definition.mode, "patch");
33
+ assert.deepEqual(Object.keys(definition.schema.getFieldDefinitions()).sort(), ["q", "workspaceSlug"]);
34
+ });
35
+
36
+ test("composeSchemaDefinitions preserves isolated schema factory registries", () => {
37
+ const localSchemaFactory = createSchema.createFactory();
38
+ localSchemaFactory.addType("scoped-status", (context) => String(context.value).toUpperCase());
39
+
40
+ const query = {
41
+ schema: localSchemaFactory({
42
+ status: {
43
+ type: "scoped-status",
44
+ required: false
45
+ }
46
+ }),
47
+ mode: "patch"
48
+ };
49
+ const params = {
50
+ schema: createSchema({
51
+ workspaceSlug: {
52
+ type: "string",
53
+ required: true,
54
+ minLength: 1
55
+ }
56
+ }),
57
+ mode: "patch"
58
+ };
59
+
60
+ const definition = composeSchemaDefinitions([params, query], {
61
+ context: "test.compose"
62
+ });
63
+ const { validatedObject } = definition.schema.patch({
64
+ workspaceSlug: "alpha",
65
+ status: "active"
66
+ });
67
+
68
+ assert.equal(validatedObject.workspaceSlug, "alpha");
69
+ assert.equal(validatedObject.status, "ACTIVE");
70
+ });
71
+
72
+ test("composeSchemaDefinitions rejects duplicate fields", () => {
73
+ const a = {
74
+ schema: createSchema({
75
+ recordId: {
76
+ type: "string",
77
+ required: true,
78
+ minLength: 1
79
+ }
80
+ }),
81
+ mode: "patch"
82
+ };
83
+ const b = {
84
+ schema: createSchema({
85
+ recordId: {
86
+ type: "string",
87
+ required: true,
88
+ minLength: 1
89
+ }
90
+ }),
91
+ mode: "patch"
92
+ };
93
+
94
+ assert.throws(
95
+ () => composeSchemaDefinitions([a, b], {
96
+ mode: "patch",
97
+ context: "test.compose"
98
+ }),
99
+ /test\.compose cannot compose duplicate field "recordId"/
100
+ );
101
+ });
102
+
103
+ test("composeSchemaDefinitions defaults to patch when all child definitions use patch mode", () => {
104
+ const params = {
105
+ schema: createSchema({
106
+ workspaceSlug: {
107
+ type: "string",
108
+ required: true,
109
+ minLength: 1
110
+ }
111
+ }),
112
+ mode: "patch"
113
+ };
114
+ const query = {
115
+ schema: createSchema({
116
+ q: {
117
+ type: "string",
118
+ required: false,
119
+ minLength: 1
120
+ }
121
+ }),
122
+ mode: "patch"
123
+ };
124
+
125
+ const definition = composeSchemaDefinitions([params, query], { context: "test.compose" });
126
+
127
+ assert.equal(definition.mode, "patch");
128
+ });
129
+
130
+ test("composeSchemaDefinitions requires an explicit mode when child definitions are not all patch", () => {
131
+ const params = {
132
+ schema: createSchema({
133
+ workspaceSlug: {
134
+ type: "string",
135
+ required: true,
136
+ minLength: 1
137
+ }
138
+ }),
139
+ mode: "patch"
140
+ };
141
+ const body = {
142
+ schema: createSchema({
143
+ name: {
144
+ type: "string",
145
+ required: true,
146
+ minLength: 1
147
+ }
148
+ }),
149
+ mode: "create"
150
+ };
151
+
152
+ assert.throws(
153
+ () => composeSchemaDefinitions([params, body], { context: "test.compose" }),
154
+ /test\.compose requires an explicit mode unless all schema definitions use patch mode/
155
+ );
156
+ });
@@ -1,41 +1,28 @@
1
- import { Type } from "typebox";
2
- import { normalizeObjectInput } from "./inputNormalization.js";
3
- import { normalizeText } from "../support/normalize.js";
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "../support/deepFreeze.js";
3
+ import { normalizeSingleSchemaDefinition } from "./schemaDefinitions.js";
4
4
 
5
5
  function createCursorListValidator(itemValidator) {
6
- if (!itemValidator || typeof itemValidator !== "object" || Array.isArray(itemValidator)) {
7
- throw new TypeError("createCursorListValidator requires an item validator object.");
8
- }
9
-
10
- if (!Object.hasOwn(itemValidator, "schema")) {
11
- throw new TypeError("createCursorListValidator requires itemValidator.schema.");
12
- }
13
-
14
- const normalizeItem =
15
- typeof itemValidator.normalize === "function"
16
- ? itemValidator.normalize
17
- : function identity(value) {
18
- return value;
19
- };
20
-
21
- return Object.freeze({
22
- get schema() {
23
- return Type.Object(
24
- {
25
- items: Type.Array(itemValidator.schema),
26
- nextCursor: Type.Union([Type.String({ minLength: 1 }), Type.Null()])
27
- },
28
- { additionalProperties: false }
29
- );
30
- },
31
- normalize(payload = {}) {
32
- const source = normalizeObjectInput(payload);
6
+ const itemDefinition = normalizeSingleSchemaDefinition(itemValidator, {
7
+ context: "cursor list item",
8
+ defaultMode: "replace"
9
+ });
33
10
 
34
- return {
35
- items: Array.isArray(source.items) ? source.items.map((entry) => normalizeItem(entry)) : [],
36
- nextCursor: normalizeText(source.nextCursor) || null
37
- };
38
- }
11
+ return deepFreeze({
12
+ schema: createSchema({
13
+ items: {
14
+ type: "array",
15
+ required: true,
16
+ items: itemDefinition.schema
17
+ },
18
+ nextCursor: {
19
+ type: "string",
20
+ required: false,
21
+ nullable: true,
22
+ minLength: 1
23
+ }
24
+ }),
25
+ mode: "replace"
39
26
  });
40
27
  }
41
28
 
@@ -1,34 +1,33 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { Type } from "typebox";
3
+ import { createSchema } from "json-rest-schema";
4
4
  import { createCursorListValidator } from "./createCursorListValidator.js";
5
5
 
6
- test("createCursorListValidator builds a list validator from an item validator", () => {
6
+ test("createCursorListValidator builds a list validator from a schema definition", () => {
7
7
  const itemValidator = {
8
- schema: Type.Object(
9
- {
10
- id: Type.Integer({ minimum: 1 }),
11
- label: Type.String({ minLength: 1 })
8
+ schema: createSchema({
9
+ id: {
10
+ type: "integer",
11
+ required: true,
12
+ min: 1
12
13
  },
13
- { additionalProperties: false }
14
- ),
15
- normalize(payload = {}) {
16
- return {
17
- id: Number(payload.id),
18
- label: String(payload.label || "").trim()
19
- };
20
- }
14
+ label: {
15
+ type: "string",
16
+ required: true,
17
+ minLength: 1
18
+ }
19
+ }),
20
+ mode: "replace"
21
21
  };
22
22
 
23
23
  const listValidator = createCursorListValidator(itemValidator);
24
- const normalized = listValidator.normalize({
25
- items: [{ id: "7", label: " member " }],
26
- nextCursor: " 8 "
27
- });
24
+ const transportSchema = listValidator.schema.toJsonSchema({ mode: listValidator.mode });
28
25
 
29
- assert.deepEqual(normalized, {
30
- items: [{ id: 7, label: "member" }],
31
- nextCursor: "8"
32
- });
33
- assert.equal(listValidator.schema.properties.items.type, "array");
26
+ assert.equal(listValidator.mode, "replace");
27
+ assert.equal(transportSchema.properties.items.type, "array");
28
+ assert.equal(transportSchema.properties.items.items["x-json-rest-schema"]?.castType, "object");
29
+ assert.equal(Array.isArray(transportSchema.properties.items.items.allOf), true);
30
+ assert.match(transportSchema.properties.items.items.allOf[0]?.$ref || "", /^#\/definitions\//);
31
+ assert.equal(transportSchema.definitions.SchemaNode_1_replace.properties.label.type, "string");
32
+ assert.equal(transportSchema.definitions.SchemaNode_1_replace.properties.label.minLength, 1);
34
33
  });
@@ -1,31 +1,21 @@
1
- import { Type } from "typebox";
2
- import { normalizeObjectInput } from "./inputNormalization.js";
3
- import { positiveIntegerValidator, recordIdInputSchema, recordIdValidator } from "./recordIdParamsValidator.js";
1
+ import { createSchema } from "json-rest-schema";
4
2
 
5
- function normalizeCursorPaginationQuery(input = {}) {
6
- const source = normalizeObjectInput(input);
7
- const normalized = {};
8
-
9
- if (Object.hasOwn(source, "cursor")) {
10
- normalized.cursor = recordIdValidator.normalize(source.cursor);
11
- }
12
-
13
- if (Object.hasOwn(source, "limit")) {
14
- normalized.limit = positiveIntegerValidator.normalize(source.limit);
3
+ const cursorPaginationQuerySchema = createSchema({
4
+ cursor: {
5
+ type: "id",
6
+ required: false
7
+ },
8
+ limit: {
9
+ type: "number",
10
+ required: false,
11
+ min: 1,
12
+ unsigned: true
15
13
  }
16
-
17
- return normalized;
18
- }
14
+ });
19
15
 
20
16
  const cursorPaginationQueryValidator = Object.freeze({
21
- schema: Type.Object(
22
- {
23
- cursor: Type.Optional(recordIdInputSchema),
24
- limit: Type.Optional(positiveIntegerValidator.schema)
25
- },
26
- { additionalProperties: false }
27
- ),
28
- normalize: normalizeCursorPaginationQuery
17
+ schema: cursorPaginationQuerySchema,
18
+ mode: "patch"
29
19
  });
30
20
 
31
21
  export {
@@ -1,26 +1,36 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { cursorPaginationQueryValidator } from "./cursorPaginationQueryValidator.js";
4
+ import { validateSchemaPayload } from "./schemaPayloadValidation.js";
4
5
 
5
- test("cursorPaginationQueryValidator normalizes numeric strings as cursor text", () => {
6
- assert.deepEqual(cursorPaginationQueryValidator.normalize({ cursor: "12", limit: "25" }), {
7
- cursor: "12",
6
+ test("cursorPaginationQueryValidator normalizes numeric strings through schema casting", () => {
7
+ assert.deepEqual(validateSchemaPayload(cursorPaginationQueryValidator, { cursor: "12", limit: "25" }), {
8
+ cursor: 12,
8
9
  limit: 25
9
10
  });
10
11
  });
11
12
 
12
13
  test("cursorPaginationQueryValidator schema rejects opaque cursor strings", () => {
14
+ const transportSchema = cursorPaginationQueryValidator.schema.toJsonSchema({ mode: "patch" });
13
15
  assert.equal(
14
- cursorPaginationQueryValidator.schema.properties.cursor.type === "string" &&
15
- cursorPaginationQueryValidator.schema.properties.cursor.pattern === "^[1-9][0-9]*$",
16
+ Array.isArray(transportSchema.properties.cursor.type) &&
17
+ transportSchema.properties.cursor.pattern === "^[1-9][0-9]*$",
16
18
  true
17
19
  );
18
20
  });
19
21
 
20
22
  test("cursorPaginationQueryValidator keeps absent keys absent", () => {
21
- assert.deepEqual(cursorPaginationQueryValidator.normalize({}), {});
23
+ assert.deepEqual(validateSchemaPayload(cursorPaginationQueryValidator, {}), {});
22
24
  });
23
25
 
24
- test("cursorPaginationQueryValidator ignores unsupported query fields", () => {
25
- assert.deepEqual(cursorPaginationQueryValidator.normalize({ q: " to " }), {});
26
+ test("cursorPaginationQueryValidator rejects unsupported query fields", () => {
27
+ assert.throws(
28
+ () => validateSchemaPayload(cursorPaginationQueryValidator, { q: " to " }),
29
+ (error) => {
30
+ assert.deepEqual(error.fieldErrors, {
31
+ q: "Field not allowed"
32
+ });
33
+ return true;
34
+ }
35
+ );
26
36
  });
@@ -1,11 +1,13 @@
1
- import { Type } from "typebox";
2
-
3
- const HTML_TIME_STRING_SCHEMA = Type.String({
1
+ const HTML_TIME_STRING_SCHEMA = Object.freeze({
2
+ type: "string",
4
3
  pattern: "^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d)?$",
5
4
  minLength: 5
6
5
  });
7
6
 
8
- const NULLABLE_HTML_TIME_STRING_SCHEMA = Type.Union([HTML_TIME_STRING_SCHEMA, Type.Null()]);
7
+ const NULLABLE_HTML_TIME_STRING_SCHEMA = Object.freeze({
8
+ ...HTML_TIME_STRING_SCHEMA,
9
+ nullable: true
10
+ });
9
11
 
10
12
  export {
11
13
  HTML_TIME_STRING_SCHEMA,
@@ -1,4 +1,6 @@
1
+ export { createSchema } from "json-rest-schema";
1
2
  export { normalizeObjectInput } from "./inputNormalization.js";
3
+ export { composeSchemaDefinitions } from "./composeSchemaDefinitions.js";
2
4
  export { createCursorListValidator } from "./createCursorListValidator.js";
3
5
  export { cursorPaginationQueryValidator } from "./cursorPaginationQueryValidator.js";
4
6
  export {
@@ -6,20 +8,26 @@ export {
6
8
  NULLABLE_HTML_TIME_STRING_SCHEMA
7
9
  } from "./htmlTimeSchemas.js";
8
10
  export { mergeObjectSchemas } from "./mergeObjectSchemas.js";
9
- export { mergeValidators } from "./mergeValidators.js";
10
- export { nestValidator } from "./nestValidator.js";
11
+ export {
12
+ hasJsonRestSchemaDefinition,
13
+ normalizeSingleSchemaDefinition,
14
+ normalizeSchemaDefinition,
15
+ resolveSchemaTransportSchemaDefinition,
16
+ resolveStructuredSchemaTransportSchema,
17
+ executeJsonRestSchemaDefinition
18
+ } from "./schemaDefinitions.js";
19
+ export {
20
+ buildSchemaValidationError,
21
+ validateSchemaPayload
22
+ } from "./schemaPayloadValidation.js";
11
23
  export {
12
24
  RECORD_ID_PATTERN,
13
25
  recordIdSchema,
14
26
  recordIdInputSchema,
15
27
  nullableRecordIdSchema,
16
28
  nullableRecordIdInputSchema,
17
- recordIdValidator,
18
- nullableRecordIdValidator,
19
- recordIdParamsValidator,
20
- positiveIntegerValidator
29
+ recordIdParamsValidator
21
30
  } from "./recordIdParamsValidator.js";
22
- export { normalizeSettingsFieldInput, normalizeSettingsFieldOutput } from "./settingsFieldNormalization.js";
23
31
  export {
24
32
  normalizeRequiredFieldList,
25
33
  deriveRequiredFieldsFromSchema,
@@ -0,0 +1,139 @@
1
+ import { normalizeObject, normalizeText } from "../support/normalize.js";
2
+
3
+ const JSON_REST_SCHEMA_MODES = new Set(["create", "replace", "patch"]);
4
+ const JSON_REST_SCHEMA_ERROR_MESSAGE_KEYS = Object.freeze({
5
+ REQUIRED: "required",
6
+ TYPE_CAST_FAILED: "default",
7
+ NOT_NULLABLE: "default",
8
+ MIN_LENGTH: "minLength",
9
+ MAX_LENGTH: "maxLength",
10
+ MIN_VALUE: "min",
11
+ MAX_VALUE: "max",
12
+ PATTERN: "pattern",
13
+ ENUM_VALUE: "enum",
14
+ FIELD_NOT_ALLOWED: "additionalProperties",
15
+ CUSTOM_VALIDATOR_FAILED: "default"
16
+ });
17
+
18
+ function isJsonRestSchemaInstance(value) {
19
+ return Boolean(value) &&
20
+ typeof value === "object" &&
21
+ typeof value.create === "function" &&
22
+ typeof value.replace === "function" &&
23
+ typeof value.patch === "function" &&
24
+ typeof value.toJsonSchema === "function";
25
+ }
26
+
27
+ function requireJsonRestSchemaInstance(schemaDefinition = null, {
28
+ context = "schema definition.schema"
29
+ } = {}) {
30
+ const schema = normalizeObject(schemaDefinition).schema;
31
+ if (!isJsonRestSchemaInstance(schema)) {
32
+ throw new TypeError(`${context} must be a json-rest-schema schema instance.`);
33
+ }
34
+
35
+ return schema;
36
+ }
37
+
38
+ function resolveSchemaDefinitionMode(schemaDefinition = null, {
39
+ defaultMode = "create",
40
+ context = "schema definition.mode"
41
+ } = {}) {
42
+ const source = normalizeObject(schemaDefinition);
43
+ const fallbackMode = normalizeText(defaultMode).toLowerCase() || "create";
44
+ const rawMode = Object.prototype.hasOwnProperty.call(source, "mode")
45
+ ? normalizeText(source.mode).toLowerCase()
46
+ : "";
47
+
48
+ if (!rawMode) {
49
+ return fallbackMode;
50
+ }
51
+
52
+ if (!JSON_REST_SCHEMA_MODES.has(rawMode)) {
53
+ throw new TypeError(`${context} must be one of: create, replace, patch.`);
54
+ }
55
+
56
+ return rawMode;
57
+ }
58
+
59
+ function resolveSchemaDefinitionTransportSchema(schemaDefinition = null, options = {}) {
60
+ const schema = requireJsonRestSchemaInstance(schemaDefinition, {
61
+ context: `${options?.context || "schema definition"}.schema`
62
+ });
63
+ const mode = resolveSchemaDefinitionMode(schemaDefinition, options);
64
+ return schema.toJsonSchema({ mode });
65
+ }
66
+
67
+ function executeSchemaDefinition(schemaDefinition = null, payload, options = {}) {
68
+ const schema = requireJsonRestSchemaInstance(schemaDefinition, {
69
+ context: `${options?.context || "schema definition"}.schema`
70
+ });
71
+ const mode = resolveSchemaDefinitionMode(schemaDefinition, options);
72
+ return schema[mode](payload);
73
+ }
74
+
75
+ function resolveJsonRestSchemaFieldMessages(schemaDefinition = null, fieldName = "") {
76
+ const normalizedFieldName = normalizeText(fieldName);
77
+ if (!normalizedFieldName) {
78
+ return {};
79
+ }
80
+
81
+ const source = normalizeObject(schemaDefinition);
82
+ if (!isJsonRestSchemaInstance(source.schema)) {
83
+ return {};
84
+ }
85
+
86
+ return normalizeObject(source.schema.getFieldMessages(normalizedFieldName));
87
+ }
88
+
89
+ function resolveJsonRestSchemaFieldErrorMessage(fieldName, entry, schemaDefinition = null) {
90
+ const messages = resolveJsonRestSchemaFieldMessages(schemaDefinition, fieldName);
91
+ const errorCode = normalizeText(entry?.code).toUpperCase();
92
+ const messageKey = JSON_REST_SCHEMA_ERROR_MESSAGE_KEYS[errorCode];
93
+
94
+ if (messageKey && typeof messages[messageKey] === "string") {
95
+ const overrideMessage = normalizeText(messages[messageKey]);
96
+ if (overrideMessage) {
97
+ return overrideMessage;
98
+ }
99
+ }
100
+
101
+ if (typeof messages.default === "string") {
102
+ const defaultMessage = normalizeText(messages.default);
103
+ if (defaultMessage) {
104
+ return defaultMessage;
105
+ }
106
+ }
107
+
108
+ return normalizeText(entry?.message || entry?.code || "Invalid value.");
109
+ }
110
+
111
+ function normalizeJsonRestSchemaFieldErrors(errors = {}, schemaDefinition = null) {
112
+ const source = normalizeObject(errors);
113
+ const fieldErrors = {};
114
+
115
+ for (const [fieldName, entry] of Object.entries(source)) {
116
+ const normalizedFieldName = normalizeText(fieldName);
117
+ if (!normalizedFieldName) {
118
+ continue;
119
+ }
120
+
121
+ if (typeof entry === "string") {
122
+ fieldErrors[normalizedFieldName] = entry;
123
+ continue;
124
+ }
125
+
126
+ const message = resolveJsonRestSchemaFieldErrorMessage(normalizedFieldName, entry, schemaDefinition);
127
+ fieldErrors[normalizedFieldName] = message || "Invalid value.";
128
+ }
129
+
130
+ return fieldErrors;
131
+ }
132
+
133
+ export {
134
+ isJsonRestSchemaInstance,
135
+ resolveSchemaDefinitionMode,
136
+ resolveSchemaDefinitionTransportSchema,
137
+ executeSchemaDefinition,
138
+ normalizeJsonRestSchemaFieldErrors
139
+ };
@@ -1,4 +1,31 @@
1
- import { Type } from "typebox";
1
+ function cloneSchemaValue(value) {
2
+ if (Array.isArray(value)) {
3
+ return value.map((entry) => cloneSchemaValue(entry));
4
+ }
5
+
6
+ if (!value || typeof value !== "object") {
7
+ return value;
8
+ }
9
+
10
+ return Object.fromEntries(
11
+ Object.entries(value).map(([key, entry]) => [key, cloneSchemaValue(entry)])
12
+ );
13
+ }
14
+
15
+ function assertMergeableObjectSchema(schema) {
16
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
17
+ throw new Error("mergeObjectSchemas only supports object schemas.");
18
+ }
19
+
20
+ const schemaType = schema.type;
21
+ if (schemaType !== "object") {
22
+ throw new Error("mergeObjectSchemas only supports object schemas.");
23
+ }
24
+
25
+ if (!schema.properties || typeof schema.properties !== "object" || Array.isArray(schema.properties)) {
26
+ throw new Error("mergeObjectSchemas only supports object schemas with properties.");
27
+ }
28
+ }
2
29
 
3
30
  function mergeObjectSchemas(schemas) {
4
31
  if (!Array.isArray(schemas)) {
@@ -6,11 +33,10 @@ function mergeObjectSchemas(schemas) {
6
33
  }
7
34
 
8
35
  const mergedProperties = {};
36
+ const required = new Set();
9
37
 
10
38
  for (const schema of schemas) {
11
- if (!schema || typeof schema !== "object" || schema.type !== "object" || typeof schema.properties !== "object") {
12
- throw new Error("mergeObjectSchemas only supports Type.Object schemas.");
13
- }
39
+ assertMergeableObjectSchema(schema);
14
40
 
15
41
  for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {
16
42
  if (Object.hasOwn(mergedProperties, propertyName) && mergedProperties[propertyName] !== propertySchema) {
@@ -19,11 +45,23 @@ function mergeObjectSchemas(schemas) {
19
45
 
20
46
  mergedProperties[propertyName] = propertySchema;
21
47
  }
48
+
49
+ for (const propertyName of Array.isArray(schema.required) ? schema.required : []) {
50
+ required.add(propertyName);
51
+ }
22
52
  }
23
53
 
24
- return Type.Object(mergedProperties, {
54
+ const mergedSchema = {
55
+ type: "object",
56
+ properties: mergedProperties,
25
57
  additionalProperties: false
26
- });
58
+ };
59
+
60
+ if (required.size > 0) {
61
+ mergedSchema.required = [...required];
62
+ }
63
+
64
+ return cloneSchemaValue(mergedSchema);
27
65
  }
28
66
 
29
67
  export {