@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.
- 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/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,57 +1,105 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
3
4
|
|
|
4
5
|
import { createRouter } from "./router.js";
|
|
5
6
|
import { compileRouteValidator, defineRouteValidator, resolveRouteValidatorOptions } from "./routeValidator.js";
|
|
6
7
|
|
|
8
|
+
function stripJsonRestTransportExtensions(value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((entry) => stripJsonRestTransportExtensions(entry));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!value || typeof value !== "object") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sanitized = {};
|
|
18
|
+
|
|
19
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
20
|
+
if (key === "x-json-rest-schema") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sanitized[key] = stripJsonRestTransportExtensions(entry);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return sanitized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toFastifySchema(schema, mode) {
|
|
31
|
+
return stripJsonRestTransportExtensions(schema.toJsonSchema({ mode }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createMockJsonRestSchema() {
|
|
35
|
+
return createSchema({
|
|
36
|
+
name: {
|
|
37
|
+
type: "string",
|
|
38
|
+
required: true,
|
|
39
|
+
minLength: 1,
|
|
40
|
+
maxLength: 160,
|
|
41
|
+
messages: {
|
|
42
|
+
minLength: "Name is required."
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
7
48
|
test("defineRouteValidator compiles body/query/params and maps query schema to querystring", () => {
|
|
8
|
-
const bodySchema = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
const bodySchema = createSchema({
|
|
50
|
+
name: {
|
|
51
|
+
type: "string",
|
|
52
|
+
required: true,
|
|
53
|
+
minLength: 1
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const querySchema = createSchema({
|
|
57
|
+
search: {
|
|
58
|
+
type: "string",
|
|
59
|
+
required: false,
|
|
60
|
+
minLength: 1
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const paramsSchema = createSchema({
|
|
64
|
+
contactId: {
|
|
65
|
+
type: "string",
|
|
66
|
+
required: true,
|
|
67
|
+
minLength: 1
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
const responseBodySchema = createSchema({
|
|
71
|
+
ok: {
|
|
72
|
+
type: "boolean",
|
|
73
|
+
required: true
|
|
74
|
+
}
|
|
75
|
+
});
|
|
17
76
|
const responseSchema = {
|
|
18
77
|
200: {
|
|
19
|
-
schema:
|
|
20
|
-
type: "object"
|
|
21
|
-
}
|
|
78
|
+
schema: responseBodySchema
|
|
22
79
|
}
|
|
23
80
|
};
|
|
24
81
|
const headersSchema = {
|
|
25
82
|
type: "object"
|
|
26
83
|
};
|
|
27
84
|
|
|
28
|
-
const normalizeBody = (body) => body;
|
|
29
|
-
const normalizeQuery = (query) => query;
|
|
30
|
-
const normalizeParams = (params) => params;
|
|
31
|
-
|
|
32
85
|
const validator = defineRouteValidator({
|
|
33
86
|
meta: {
|
|
34
87
|
tags: ["contacts", "intake"],
|
|
35
88
|
summary: "Create contact intake"
|
|
36
89
|
},
|
|
37
|
-
|
|
38
|
-
schema: bodySchema
|
|
39
|
-
normalize: normalizeBody
|
|
90
|
+
body: {
|
|
91
|
+
schema: bodySchema
|
|
40
92
|
},
|
|
41
|
-
|
|
42
|
-
schema: querySchema
|
|
43
|
-
normalize: normalizeQuery
|
|
93
|
+
query: {
|
|
94
|
+
schema: querySchema
|
|
44
95
|
},
|
|
45
|
-
|
|
96
|
+
params: {
|
|
46
97
|
schema: paramsSchema
|
|
47
98
|
},
|
|
48
|
-
|
|
99
|
+
responses: responseSchema,
|
|
49
100
|
advanced: {
|
|
50
101
|
fastifySchema: {
|
|
51
102
|
headers: headersSchema
|
|
52
|
-
},
|
|
53
|
-
jskitInput: {
|
|
54
|
-
params: normalizeParams
|
|
55
103
|
}
|
|
56
104
|
}
|
|
57
105
|
});
|
|
@@ -61,62 +109,73 @@ test("defineRouteValidator compiles body/query/params and maps query schema to q
|
|
|
61
109
|
assert.deepEqual(compiled.schema, {
|
|
62
110
|
tags: ["contacts", "intake"],
|
|
63
111
|
summary: "Create contact intake",
|
|
64
|
-
body: bodySchema,
|
|
65
|
-
querystring: querySchema,
|
|
66
|
-
params: paramsSchema,
|
|
112
|
+
body: toFastifySchema(bodySchema, "patch"),
|
|
113
|
+
querystring: toFastifySchema(querySchema, "patch"),
|
|
114
|
+
params: toFastifySchema(paramsSchema, "patch"),
|
|
67
115
|
response: {
|
|
68
|
-
200:
|
|
69
|
-
type: "object"
|
|
70
|
-
}
|
|
116
|
+
200: toFastifySchema(responseBodySchema, "replace")
|
|
71
117
|
},
|
|
72
118
|
headers: headersSchema
|
|
73
119
|
});
|
|
74
|
-
assert.equal(compiled.input.body,
|
|
75
|
-
assert.equal(compiled.input.query,
|
|
76
|
-
assert.equal(compiled.input.params,
|
|
120
|
+
assert.equal(typeof compiled.input.body, "function");
|
|
121
|
+
assert.equal(typeof compiled.input.query, "function");
|
|
122
|
+
assert.equal(typeof compiled.input.params, "function");
|
|
123
|
+
assert.deepEqual(compiled.input.body({
|
|
124
|
+
name: " Acme "
|
|
125
|
+
}), {
|
|
126
|
+
name: "Acme"
|
|
127
|
+
});
|
|
77
128
|
});
|
|
78
129
|
|
|
79
|
-
test("compileRouteValidator accepts
|
|
80
|
-
const querySchema = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
test("compileRouteValidator accepts json-rest-schema definitions", () => {
|
|
131
|
+
const querySchema = createSchema({
|
|
132
|
+
search: {
|
|
133
|
+
type: "string",
|
|
134
|
+
required: false,
|
|
135
|
+
minLength: 1
|
|
136
|
+
}
|
|
85
137
|
});
|
|
86
138
|
|
|
87
139
|
const compiled = compileRouteValidator({
|
|
88
|
-
|
|
89
|
-
schema: querySchema
|
|
90
|
-
normalize: normalizeQuery
|
|
140
|
+
query: {
|
|
141
|
+
schema: querySchema
|
|
91
142
|
}
|
|
92
143
|
});
|
|
93
144
|
|
|
94
145
|
assert.deepEqual(compiled.schema, {
|
|
95
|
-
querystring: querySchema
|
|
146
|
+
querystring: toFastifySchema(querySchema, "patch")
|
|
96
147
|
});
|
|
97
|
-
assert.equal(compiled.input.query,
|
|
148
|
+
assert.equal(typeof compiled.input.query, "function");
|
|
98
149
|
});
|
|
99
150
|
|
|
100
|
-
test("compileRouteValidator creates
|
|
101
|
-
const querySchema = {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
151
|
+
test("compileRouteValidator creates request.input transforms for schema-only params and query", () => {
|
|
152
|
+
const querySchema = createSchema({
|
|
153
|
+
workspaceSlug: {
|
|
154
|
+
type: "string",
|
|
155
|
+
required: false,
|
|
156
|
+
minLength: 1
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const paramsSchema = createSchema({
|
|
160
|
+
workspaceSlug: {
|
|
161
|
+
type: "string",
|
|
162
|
+
required: true,
|
|
163
|
+
minLength: 1
|
|
164
|
+
}
|
|
165
|
+
});
|
|
107
166
|
|
|
108
167
|
const compiled = compileRouteValidator({
|
|
109
|
-
|
|
168
|
+
query: {
|
|
110
169
|
schema: querySchema
|
|
111
170
|
},
|
|
112
|
-
|
|
171
|
+
params: {
|
|
113
172
|
schema: paramsSchema
|
|
114
173
|
}
|
|
115
174
|
});
|
|
116
175
|
|
|
117
176
|
assert.deepEqual(compiled.schema, {
|
|
118
|
-
querystring: querySchema,
|
|
119
|
-
params: paramsSchema
|
|
177
|
+
querystring: toFastifySchema(querySchema, "patch"),
|
|
178
|
+
params: toFastifySchema(paramsSchema, "patch")
|
|
120
179
|
});
|
|
121
180
|
assert.equal(typeof compiled.input.query, "function");
|
|
122
181
|
assert.equal(typeof compiled.input.params, "function");
|
|
@@ -124,202 +183,111 @@ test("compileRouteValidator creates pass-through request.input transforms for sc
|
|
|
124
183
|
assert.deepEqual(compiled.input.params({ workspaceSlug: "acme" }), { workspaceSlug: "acme" });
|
|
125
184
|
});
|
|
126
185
|
|
|
127
|
-
test("compileRouteValidator accepts response
|
|
128
|
-
const responseBodySchema = {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
normalized: true
|
|
186
|
+
test("compileRouteValidator accepts response schema definitions and extracts only response schemas", () => {
|
|
187
|
+
const responseBodySchema = createSchema({
|
|
188
|
+
ok: {
|
|
189
|
+
type: "boolean",
|
|
190
|
+
required: true
|
|
191
|
+
}
|
|
134
192
|
});
|
|
135
193
|
|
|
136
194
|
const compiled = compileRouteValidator({
|
|
137
|
-
|
|
195
|
+
responses: {
|
|
138
196
|
200: {
|
|
139
|
-
schema: responseBodySchema
|
|
140
|
-
normalize: normalizeOutput
|
|
197
|
+
schema: responseBodySchema
|
|
141
198
|
},
|
|
142
199
|
400: {
|
|
143
|
-
schema: {
|
|
144
|
-
|
|
145
|
-
|
|
200
|
+
schema: createSchema({
|
|
201
|
+
ok: {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
required: true
|
|
204
|
+
}
|
|
205
|
+
})
|
|
146
206
|
}
|
|
147
207
|
}
|
|
148
208
|
});
|
|
149
209
|
|
|
150
210
|
assert.deepEqual(compiled.schema, {
|
|
151
211
|
response: {
|
|
152
|
-
200: responseBodySchema,
|
|
153
|
-
400: {
|
|
154
|
-
|
|
155
|
-
|
|
212
|
+
200: toFastifySchema(responseBodySchema, "replace"),
|
|
213
|
+
400: toFastifySchema(createSchema({
|
|
214
|
+
ok: {
|
|
215
|
+
type: "boolean",
|
|
216
|
+
required: true
|
|
217
|
+
}
|
|
218
|
+
}), "replace")
|
|
156
219
|
}
|
|
157
220
|
});
|
|
158
|
-
assert.equal(Object.
|
|
221
|
+
assert.equal(Object.hasOwn(compiled, "output"), false);
|
|
159
222
|
});
|
|
160
223
|
|
|
161
|
-
test("compileRouteValidator
|
|
162
|
-
const
|
|
163
|
-
schema: {
|
|
164
|
-
type: "object",
|
|
165
|
-
properties: {
|
|
166
|
-
cursor: {
|
|
167
|
-
type: "string"
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
additionalProperties: false
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
const searchQuery = {
|
|
174
|
-
schema: {
|
|
175
|
-
type: "object",
|
|
176
|
-
properties: {
|
|
177
|
-
search: {
|
|
178
|
-
type: "string"
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
additionalProperties: false
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
224
|
+
test("compileRouteValidator turns json-rest-schema validators into transport schema plus input normalization", () => {
|
|
225
|
+
const bodySchema = createMockJsonRestSchema();
|
|
185
226
|
const compiled = compileRouteValidator({
|
|
186
|
-
|
|
227
|
+
body: {
|
|
228
|
+
schema: bodySchema,
|
|
229
|
+
mode: "patch"
|
|
230
|
+
}
|
|
187
231
|
});
|
|
188
232
|
|
|
189
233
|
assert.deepEqual(compiled.schema, {
|
|
190
|
-
|
|
191
|
-
type: "object",
|
|
192
|
-
properties: {
|
|
193
|
-
cursor: {
|
|
194
|
-
type: "string"
|
|
195
|
-
},
|
|
196
|
-
search: {
|
|
197
|
-
type: "string"
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
required: ["cursor", "search"],
|
|
201
|
-
additionalProperties: false
|
|
202
|
-
}
|
|
234
|
+
body: toFastifySchema(bodySchema, "patch")
|
|
203
235
|
});
|
|
204
|
-
});
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
required: ["workspaceSlug"],
|
|
216
|
-
additionalProperties: false
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
const inviteIdParams = {
|
|
220
|
-
schema: {
|
|
221
|
-
type: "object",
|
|
222
|
-
properties: {
|
|
223
|
-
inviteId: {
|
|
224
|
-
type: "string"
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
required: ["inviteId"],
|
|
228
|
-
additionalProperties: false
|
|
229
|
-
}
|
|
230
|
-
};
|
|
237
|
+
const normalized = compiled.input.body({
|
|
238
|
+
name: " Acme "
|
|
239
|
+
});
|
|
240
|
+
assert.deepEqual(normalized, {
|
|
241
|
+
name: "Acme"
|
|
242
|
+
});
|
|
243
|
+
});
|
|
231
244
|
|
|
245
|
+
test("compileRouteValidator surfaces shared schema validation errors with HTTP 400 metadata", () => {
|
|
246
|
+
const bodySchema = createMockJsonRestSchema();
|
|
232
247
|
const compiled = compileRouteValidator({
|
|
233
|
-
|
|
248
|
+
body: {
|
|
249
|
+
schema: bodySchema,
|
|
250
|
+
mode: "patch"
|
|
251
|
+
}
|
|
234
252
|
});
|
|
235
253
|
|
|
236
|
-
assert.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
type: "string"
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
required: ["workspaceSlug", "inviteId"],
|
|
248
|
-
additionalProperties: false
|
|
254
|
+
assert.throws(
|
|
255
|
+
() => compiled.input.body({ name: "" }),
|
|
256
|
+
(error) => {
|
|
257
|
+
assert.equal(error?.statusCode, 400);
|
|
258
|
+
assert.deepEqual(error?.details?.fieldErrors, {
|
|
259
|
+
name: "Name is required."
|
|
260
|
+
});
|
|
261
|
+
return true;
|
|
249
262
|
}
|
|
250
|
-
|
|
263
|
+
);
|
|
251
264
|
});
|
|
252
265
|
|
|
253
|
-
test("compileRouteValidator
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
properties: {
|
|
266
|
+
test("compileRouteValidator rejects validator arrays", () => {
|
|
267
|
+
assert.throws(
|
|
268
|
+
() => compileRouteValidator({
|
|
269
|
+
query: [
|
|
270
|
+
{
|
|
271
|
+
schema: createSchema({
|
|
260
272
|
cursor: {
|
|
261
|
-
type: "string"
|
|
262
|
-
|
|
263
|
-
},
|
|
264
|
-
additionalProperties: false
|
|
265
|
-
},
|
|
266
|
-
normalize(query = {}) {
|
|
267
|
-
return {
|
|
268
|
-
cursor: String(query.cursor || "").trim()
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
schema: {
|
|
274
|
-
type: "object",
|
|
275
|
-
properties: {
|
|
276
|
-
search: {
|
|
277
|
-
type: "string"
|
|
273
|
+
type: "string",
|
|
274
|
+
required: false
|
|
278
275
|
}
|
|
279
|
-
}
|
|
280
|
-
additionalProperties: false
|
|
281
|
-
},
|
|
282
|
-
normalize(query = {}) {
|
|
283
|
-
return {
|
|
284
|
-
search: String(query.search || "").trim().toLowerCase()
|
|
285
|
-
};
|
|
276
|
+
})
|
|
286
277
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
assert.deepEqual(compiled.input.query({ cursor: " 100 ", search: " ACME " }), {
|
|
292
|
-
cursor: "100",
|
|
293
|
-
search: "acme"
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("resolveRouteValidatorOptions ignores legacy schema/input definitions", () => {
|
|
298
|
-
const resolved = resolveRouteValidatorOptions({
|
|
299
|
-
method: "POST",
|
|
300
|
-
path: "/contacts",
|
|
301
|
-
options: {
|
|
302
|
-
schema: {
|
|
303
|
-
bodyValidator: {}
|
|
304
|
-
},
|
|
305
|
-
input: {
|
|
306
|
-
body: () => ({})
|
|
307
|
-
},
|
|
308
|
-
middleware: ["api"]
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
assert.equal(Object.prototype.hasOwnProperty.call(resolved, "schema"), false);
|
|
313
|
-
assert.equal(Object.prototype.hasOwnProperty.call(resolved, "input"), false);
|
|
314
|
-
assert.deepEqual(resolved.middleware, ["api"]);
|
|
278
|
+
]
|
|
279
|
+
}),
|
|
280
|
+
/route validator\.query must be a schema definition object/
|
|
281
|
+
);
|
|
315
282
|
});
|
|
316
283
|
|
|
317
284
|
test("resolveRouteValidatorOptions supports inline validator shape without wrapper", () => {
|
|
318
|
-
const bodySchema = {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
285
|
+
const bodySchema = createSchema({
|
|
286
|
+
name: {
|
|
287
|
+
type: "string",
|
|
288
|
+
required: true,
|
|
289
|
+
minLength: 1
|
|
290
|
+
}
|
|
323
291
|
});
|
|
324
292
|
|
|
325
293
|
const resolved = resolveRouteValidatorOptions({
|
|
@@ -330,9 +298,8 @@ test("resolveRouteValidatorOptions supports inline validator shape without wrapp
|
|
|
330
298
|
tags: ["contacts"],
|
|
331
299
|
summary: "Create contact"
|
|
332
300
|
},
|
|
333
|
-
|
|
334
|
-
schema: bodySchema
|
|
335
|
-
normalize: normalizeBody
|
|
301
|
+
body: {
|
|
302
|
+
schema: bodySchema
|
|
336
303
|
},
|
|
337
304
|
middleware: ["api"]
|
|
338
305
|
}
|
|
@@ -341,37 +308,73 @@ test("resolveRouteValidatorOptions supports inline validator shape without wrapp
|
|
|
341
308
|
assert.deepEqual(resolved.schema, {
|
|
342
309
|
tags: ["contacts"],
|
|
343
310
|
summary: "Create contact",
|
|
344
|
-
body: bodySchema
|
|
311
|
+
body: toFastifySchema(bodySchema, "patch")
|
|
312
|
+
});
|
|
313
|
+
assert.equal(typeof resolved.input.body, "function");
|
|
314
|
+
assert.deepEqual(resolved.input.body({
|
|
315
|
+
name: " Ada "
|
|
316
|
+
}), {
|
|
317
|
+
name: "Ada"
|
|
345
318
|
});
|
|
346
|
-
assert.equal(resolved.input.body, normalizeBody);
|
|
347
319
|
assert.deepEqual(resolved.middleware, ["api"]);
|
|
348
320
|
});
|
|
349
321
|
|
|
350
|
-
test("resolveRouteValidatorOptions
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
322
|
+
test("resolveRouteValidatorOptions rejects schema/input definitions", () => {
|
|
323
|
+
assert.throws(
|
|
324
|
+
() => resolveRouteValidatorOptions({
|
|
325
|
+
method: "POST",
|
|
326
|
+
path: "/contacts",
|
|
327
|
+
options: {
|
|
328
|
+
schema: {
|
|
329
|
+
body: {}
|
|
330
|
+
},
|
|
331
|
+
input: {
|
|
332
|
+
body: () => ({})
|
|
333
|
+
},
|
|
334
|
+
middleware: ["api"]
|
|
335
|
+
}
|
|
336
|
+
}),
|
|
337
|
+
/uses unsupported validator options: schema, input/
|
|
338
|
+
);
|
|
339
|
+
});
|
|
359
340
|
|
|
360
|
-
|
|
361
|
-
assert.
|
|
341
|
+
test("resolveRouteValidatorOptions rejects validator wrapper", () => {
|
|
342
|
+
assert.throws(
|
|
343
|
+
() => resolveRouteValidatorOptions({
|
|
344
|
+
method: "POST",
|
|
345
|
+
path: "/contacts",
|
|
346
|
+
options: {
|
|
347
|
+
validator: defineRouteValidator({}),
|
|
348
|
+
middleware: ["api"]
|
|
349
|
+
}
|
|
350
|
+
}),
|
|
351
|
+
/uses unsupported validator options: validator/
|
|
352
|
+
);
|
|
362
353
|
});
|
|
363
354
|
|
|
364
|
-
test("defineRouteValidator rejects unsupported advanced.jskitInput
|
|
355
|
+
test("defineRouteValidator rejects unsupported advanced.jskitInput", () => {
|
|
365
356
|
assert.throws(
|
|
366
357
|
() =>
|
|
367
358
|
defineRouteValidator({
|
|
368
359
|
advanced: {
|
|
369
|
-
jskitInput: {
|
|
370
|
-
|
|
360
|
+
jskitInput: {}
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
/advanced\.jskitInput is not supported/
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("defineRouteValidator rejects unsupported top-level keys generically", () => {
|
|
368
|
+
assert.throws(
|
|
369
|
+
() =>
|
|
370
|
+
defineRouteValidator({
|
|
371
|
+
unsupportedContract: {
|
|
372
|
+
schema: {
|
|
373
|
+
type: "object"
|
|
371
374
|
}
|
|
372
375
|
}
|
|
373
376
|
}),
|
|
374
|
-
/
|
|
377
|
+
/defineRouteValidator\(\)\.unsupportedContract is not supported/
|
|
375
378
|
);
|
|
376
379
|
});
|
|
377
380
|
|
|
@@ -397,56 +400,55 @@ test("defineRouteValidator validates meta fields", () => {
|
|
|
397
400
|
);
|
|
398
401
|
});
|
|
399
402
|
|
|
400
|
-
test("HttpRouter.register
|
|
403
|
+
test("HttpRouter.register rejects validator wrapper options", () => {
|
|
401
404
|
const router = createRouter();
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
405
|
+
assert.throws(
|
|
406
|
+
() => router.register(
|
|
407
|
+
"POST",
|
|
408
|
+
"/contacts",
|
|
409
|
+
{
|
|
410
|
+
validator: defineRouteValidator({})
|
|
411
|
+
},
|
|
412
|
+
async () => {}
|
|
413
|
+
),
|
|
414
|
+
/uses unsupported validator options: validator/
|
|
409
415
|
);
|
|
410
|
-
|
|
411
|
-
const [route] = router.list();
|
|
412
|
-
assert.equal(route.schema, undefined);
|
|
413
|
-
assert.equal(route.input, null);
|
|
414
416
|
});
|
|
415
417
|
|
|
416
|
-
test("HttpRouter.register
|
|
418
|
+
test("HttpRouter.register rejects compiled route option payloads", () => {
|
|
417
419
|
const router = createRouter();
|
|
418
|
-
const querySchema = {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
420
|
+
const querySchema = createSchema({
|
|
421
|
+
dryRun: {
|
|
422
|
+
type: "boolean",
|
|
423
|
+
required: false,
|
|
424
|
+
strictBoolean: true
|
|
425
|
+
}
|
|
423
426
|
});
|
|
424
427
|
|
|
425
428
|
const validator = defineRouteValidator({
|
|
426
|
-
|
|
427
|
-
schema: querySchema
|
|
428
|
-
normalize: normalizeQuery
|
|
429
|
+
query: {
|
|
430
|
+
schema: querySchema
|
|
429
431
|
}
|
|
430
432
|
});
|
|
431
433
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
assert.throws(
|
|
435
|
+
() => router.get(
|
|
436
|
+
"/contacts",
|
|
437
|
+
validator.toRouteOptions(),
|
|
438
|
+
async () => {}
|
|
439
|
+
),
|
|
440
|
+
/uses unsupported validator options: schema, input/
|
|
436
441
|
);
|
|
437
|
-
|
|
438
|
-
const [route] = router.list();
|
|
439
|
-
assert.equal(route.schema, undefined);
|
|
440
|
-
assert.equal(route.input, null);
|
|
441
442
|
});
|
|
442
443
|
|
|
443
444
|
test("HttpRouter.register accepts inline validator shape directly", () => {
|
|
444
445
|
const router = createRouter();
|
|
445
|
-
const querySchema = {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
446
|
+
const querySchema = createSchema({
|
|
447
|
+
dryRun: {
|
|
448
|
+
type: "boolean",
|
|
449
|
+
required: false,
|
|
450
|
+
strictBoolean: true
|
|
451
|
+
}
|
|
450
452
|
});
|
|
451
453
|
|
|
452
454
|
router.get(
|
|
@@ -456,9 +458,8 @@ test("HttpRouter.register accepts inline validator shape directly", () => {
|
|
|
456
458
|
tags: ["contacts"],
|
|
457
459
|
summary: "List contacts"
|
|
458
460
|
},
|
|
459
|
-
|
|
460
|
-
schema: querySchema
|
|
461
|
-
normalize: normalizeQuery
|
|
461
|
+
query: {
|
|
462
|
+
schema: querySchema
|
|
462
463
|
}
|
|
463
464
|
},
|
|
464
465
|
async () => {}
|
|
@@ -468,7 +469,113 @@ test("HttpRouter.register accepts inline validator shape directly", () => {
|
|
|
468
469
|
assert.deepEqual(route.schema, {
|
|
469
470
|
tags: ["contacts"],
|
|
470
471
|
summary: "List contacts",
|
|
471
|
-
querystring: querySchema
|
|
472
|
+
querystring: toFastifySchema(querySchema, "patch")
|
|
473
|
+
});
|
|
474
|
+
assert.equal(typeof route.input.query, "function");
|
|
475
|
+
assert.deepEqual(route.input.query({
|
|
476
|
+
dryRun: true
|
|
477
|
+
}), {
|
|
478
|
+
dryRun: true
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("HttpRouter.register accepts explicit transport metadata and output transform", () => {
|
|
483
|
+
const router = createRouter();
|
|
484
|
+
const output = (payload) => ({
|
|
485
|
+
data: payload
|
|
486
|
+
});
|
|
487
|
+
const error = (currentError) => ({
|
|
488
|
+
errors: [{
|
|
489
|
+
title: currentError.message
|
|
490
|
+
}]
|
|
472
491
|
});
|
|
473
|
-
|
|
492
|
+
|
|
493
|
+
router.get(
|
|
494
|
+
"/contacts",
|
|
495
|
+
{
|
|
496
|
+
transport: {
|
|
497
|
+
kind: "jsonapi-resource",
|
|
498
|
+
contentType: "application/vnd.api+json",
|
|
499
|
+
request: {
|
|
500
|
+
body(body) {
|
|
501
|
+
return body?.data?.attributes || {};
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
error
|
|
505
|
+
},
|
|
506
|
+
output
|
|
507
|
+
},
|
|
508
|
+
async () => {}
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const [route] = router.list();
|
|
512
|
+
assert.equal(route.transport.kind, "jsonapi-resource");
|
|
513
|
+
assert.equal(route.transport.contentType, "application/vnd.api+json");
|
|
514
|
+
assert.equal(typeof route.transport.request.body, "function");
|
|
515
|
+
assert.equal(route.transport.error, error);
|
|
516
|
+
assert.equal(route.output, output);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("HttpRouter.register rejects invalid transport definitions and output transforms", () => {
|
|
520
|
+
const router = createRouter();
|
|
521
|
+
|
|
522
|
+
assert.throws(
|
|
523
|
+
() => router.get(
|
|
524
|
+
"/contacts",
|
|
525
|
+
{
|
|
526
|
+
transport: {
|
|
527
|
+
kind: "weird"
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
async () => {}
|
|
531
|
+
),
|
|
532
|
+
/transport\.kind must be one of: command, jsonapi-resource/
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
assert.throws(
|
|
536
|
+
() => router.get(
|
|
537
|
+
"/contacts",
|
|
538
|
+
{
|
|
539
|
+
output: {
|
|
540
|
+
mode: "unsupported"
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
async () => {}
|
|
544
|
+
),
|
|
545
|
+
/output must be a function/
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("compileRouteValidator strips json-rest transport metadata before Fastify handoff", () => {
|
|
550
|
+
const nestedSchema = createSchema({
|
|
551
|
+
profile: {
|
|
552
|
+
type: "object",
|
|
553
|
+
required: false,
|
|
554
|
+
schema: createSchema({
|
|
555
|
+
email: {
|
|
556
|
+
type: "string",
|
|
557
|
+
required: false,
|
|
558
|
+
minLength: 3,
|
|
559
|
+
format: "email"
|
|
560
|
+
}
|
|
561
|
+
}),
|
|
562
|
+
additionalProperties: true
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const compiled = compileRouteValidator({
|
|
567
|
+
body: {
|
|
568
|
+
schema: nestedSchema
|
|
569
|
+
},
|
|
570
|
+
responses: {
|
|
571
|
+
200: {
|
|
572
|
+
schema: nestedSchema,
|
|
573
|
+
mode: "replace"
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
assert.equal(JSON.stringify(compiled.schema).includes("x-json-rest-schema"), false);
|
|
579
|
+
assert.deepEqual(compiled.schema.body, toFastifySchema(nestedSchema, "patch"));
|
|
580
|
+
assert.deepEqual(compiled.schema.response[200], toFastifySchema(nestedSchema, "replace"));
|
|
474
581
|
});
|