@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
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
{
|
|
39
|
+
type: "object",
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
properties: {
|
|
42
|
+
workspaceSlug: {
|
|
43
|
+
type: "string",
|
|
44
|
+
minLength: 1
|
|
45
|
+
}
|
|
34
46
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 =
|
|
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 =
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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?.
|
|
27
|
-
const replaceSchema = operations?.replace?.
|
|
28
|
-
const patchSchema = operations?.patch?.
|
|
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 =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
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 =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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: {
|
|
39
|
-
replace: {
|
|
40
|
-
patch: {
|
|
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
|
|
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
|
-
|
|
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);
|