@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.
- package/package.json +3 -2
- package/server/actions/ActionRuntimeServiceProvider.test.js +23 -15
- package/server/http/lib/kernel.test.js +447 -0
- package/server/http/lib/routeRegistration.js +236 -15
- package/server/http/lib/routeTransport.js +126 -0
- package/server/http/lib/routeValidator.js +133 -198
- package/server/http/lib/routeValidator.test.js +385 -278
- package/server/http/lib/router.js +17 -2
- package/server/platform/providerRuntime.test.js +7 -7
- package/server/runtime/bootBootstrapRoutes.js +2 -18
- package/server/runtime/bootBootstrapRoutes.test.js +5 -14
- package/server/runtime/fastifyBootstrap.js +119 -0
- package/server/runtime/fastifyBootstrap.test.js +119 -1
- package/server/runtime/moduleConfig.js +32 -62
- package/server/runtime/moduleConfig.test.js +48 -24
- package/server/support/pageTargets.js +15 -9
- package/server/support/pageTargets.test.js +1 -1
- package/shared/actions/actionContributorHelpers.js +5 -11
- package/shared/actions/actionDefinitions.js +37 -150
- package/shared/actions/actionDefinitions.test.js +117 -136
- package/shared/actions/policies.js +25 -169
- package/shared/actions/policies.test.js +76 -87
- package/shared/actions/registry.test.js +24 -50
- package/shared/support/crudFieldContract.js +322 -0
- package/shared/support/crudFieldContract.test.js +67 -0
- package/shared/support/crudListFilters.js +582 -38
- package/shared/support/crudListFilters.test.js +178 -8
- package/shared/support/crudLookup.js +14 -7
- package/shared/support/crudLookup.test.js +91 -66
- package/shared/support/normalize.js +7 -0
- package/shared/support/normalize.test.js +4 -2
- package/shared/support/shellLayoutTargets.test.js +1 -1
- package/shared/validators/composeSchemaDefinitions.js +53 -0
- package/shared/validators/composeSchemaDefinitions.test.js +156 -0
- package/shared/validators/createCursorListValidator.js +22 -35
- package/shared/validators/createCursorListValidator.test.js +22 -23
- package/shared/validators/cursorPaginationQueryValidator.js +14 -24
- package/shared/validators/cursorPaginationQueryValidator.test.js +18 -8
- package/shared/validators/htmlTimeSchemas.js +6 -4
- package/shared/validators/index.js +15 -7
- package/shared/validators/jsonRestSchemaSupport.js +139 -0
- package/shared/validators/mergeObjectSchemas.js +44 -6
- package/shared/validators/mergeObjectSchemas.test.js +60 -35
- package/shared/validators/recordIdParamsValidator.js +19 -52
- package/shared/validators/recordIdParamsValidator.test.js +13 -8
- package/shared/validators/resourceRequiredMetadata.js +3 -3
- package/shared/validators/resourceRequiredMetadata.test.js +29 -16
- package/shared/validators/schemaDefinitions.js +126 -0
- package/shared/validators/schemaDefinitions.test.js +51 -0
- package/shared/validators/schemaPayloadValidation.js +65 -0
- package/test/barrelExposure.test.js +30 -0
- package/test/routeInputContractGuard.test.js +10 -6
- package/shared/validators/mergeValidators.js +0 -89
- package/shared/validators/mergeValidators.test.js +0 -116
- package/shared/validators/nestValidator.js +0 -53
- package/shared/validators/nestValidator.test.js +0 -60
- 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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 {
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
4
4
|
import { createCursorListValidator } from "./createCursorListValidator.js";
|
|
5
5
|
|
|
6
|
-
test("createCursorListValidator builds a list validator from
|
|
6
|
+
test("createCursorListValidator builds a list validator from a schema definition", () => {
|
|
7
7
|
const itemValidator = {
|
|
8
|
-
schema:
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
schema: createSchema({
|
|
9
|
+
id: {
|
|
10
|
+
type: "integer",
|
|
11
|
+
required: true,
|
|
12
|
+
min: 1
|
|
12
13
|
},
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
items: [{ id: "7", label: " member " }],
|
|
26
|
-
nextCursor: " 8 "
|
|
27
|
-
});
|
|
24
|
+
const transportSchema = listValidator.schema.toJsonSchema({ mode: listValidator.mode });
|
|
28
25
|
|
|
29
|
-
assert.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
assert.
|
|
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 {
|
|
2
|
-
import { normalizeObjectInput } from "./inputNormalization.js";
|
|
3
|
-
import { positiveIntegerValidator, recordIdInputSchema, recordIdValidator } from "./recordIdParamsValidator.js";
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
|
6
|
-
assert.deepEqual(cursorPaginationQueryValidator
|
|
7
|
-
cursor:
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
23
|
+
assert.deepEqual(validateSchemaPayload(cursorPaginationQueryValidator, {}), {});
|
|
22
24
|
});
|
|
23
25
|
|
|
24
|
-
test("cursorPaginationQueryValidator
|
|
25
|
-
assert.
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|