@jskit-ai/http-runtime 0.1.243 → 0.1.244
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 +6 -35
- package/src/client/index.js +2 -3
- package/src/shared/clientRuntime/client.js +1 -57
- package/src/shared/clientRuntime/errors.js +39 -2
- package/src/shared/clientRuntime/jsonApiResourceTransport.js +27 -39
- package/src/shared/index.js +2 -21
- package/src/shared/validators/errorResponses.js +1 -13
- package/src/shared/validators/jsonApiQueryTransport.js +1 -5
- package/src/shared/validators/jsonApiRouteTransport.js +43 -54
- package/src/shared/validators/jsonApiTransport.js +1 -5
- package/src/shared/validators/operationValidation.js +5 -92
- package/test/client.test.js +29 -0
- package/test/entrypoints.boundary.test.js +15 -14
- package/test/errorResponses.test.js +0 -11
- package/test/jsonApiRouteTransport.test.js +12 -7
- package/test/operationValidation.test.js +13 -61
- package/test/transientRetryHttpClient.test.js +11 -0
- package/test/validationErrors.test.js +20 -9
- package/src/client/providers/HttpClientRuntimeClientProvider.js +0 -20
- package/src/client/providers/HttpValidatorsClientProvider.js +0 -16
- package/src/server/providers/HttpClientProvider.js +0 -16
- package/src/server/providers/HttpValidatorsProvider.js +0 -16
- package/src/shared/clientRuntime/index.js +0 -4
- package/src/shared/validators/command.js +0 -24
- package/src/shared/validators/httpValidatorsApi.js +0 -129
- package/src/shared/validators/jsonApiResponses.js +0 -3
- package/src/shared/validators/operationMessages.js +0 -318
- package/src/shared/validators/paginationQuery.js +0 -27
- package/src/shared/validators/resource.js +0 -41
- package/test/command.test.js +0 -66
- package/test/operationMessages.test.js +0 -197
- package/test/paginationQuery.test.js +0 -28
- package/test/providerRuntime.httpClient.test.js +0 -22
- package/test/providerRuntime.validators.test.js +0 -22
- package/test/resource.test.js +0 -128
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import { isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
-
|
|
3
|
-
function resolveIssueField(issue = {}) {
|
|
4
|
-
const instancePath = normalizeText(issue.instancePath);
|
|
5
|
-
if (instancePath) {
|
|
6
|
-
const segments = instancePath
|
|
7
|
-
.replace(/^\//, "")
|
|
8
|
-
.split("/")
|
|
9
|
-
.map((entry) => normalizeText(entry))
|
|
10
|
-
.filter(Boolean);
|
|
11
|
-
|
|
12
|
-
if (segments.length > 0) {
|
|
13
|
-
return segments[0];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const params = isRecord(issue.params) ? issue.params : {};
|
|
18
|
-
const missingProperty = normalizeText(params.missingProperty);
|
|
19
|
-
if (missingProperty) {
|
|
20
|
-
return missingProperty;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const requiredProperties = Array.isArray(params.requiredProperties)
|
|
24
|
-
? params.requiredProperties
|
|
25
|
-
: [];
|
|
26
|
-
if (requiredProperties.length > 0) {
|
|
27
|
-
return normalizeText(requiredProperties[0]);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const additionalProperties = Array.isArray(params.additionalProperties)
|
|
31
|
-
? params.additionalProperties
|
|
32
|
-
: [];
|
|
33
|
-
if (additionalProperties.length > 0) {
|
|
34
|
-
return normalizeText(additionalProperties[0]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const additionalProperty = normalizeText(params.additionalProperty);
|
|
38
|
-
if (additionalProperty) {
|
|
39
|
-
return additionalProperty;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function decodePointerSegment(value = "") {
|
|
46
|
-
return String(value).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resolveSchemaPathSegments(schemaPath = "") {
|
|
50
|
-
const normalizedSchemaPath = normalizeText(schemaPath);
|
|
51
|
-
if (!normalizedSchemaPath.startsWith("#")) {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const pointer = normalizedSchemaPath.slice(1);
|
|
56
|
-
if (!pointer) {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return pointer
|
|
61
|
-
.replace(/^\//, "")
|
|
62
|
-
.split("/")
|
|
63
|
-
.map((segment) => decodePointerSegment(segment))
|
|
64
|
-
.filter((segment) => segment.length > 0);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function resolveFieldFromSchemaPath(schemaPath = "") {
|
|
68
|
-
const segments = resolveSchemaPathSegments(schemaPath);
|
|
69
|
-
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
70
|
-
if (segments[index] !== "properties") {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const field = normalizeText(segments[index + 1]);
|
|
75
|
-
if (field) {
|
|
76
|
-
return field;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return "";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolveSchemaNode(schema = {}, schemaPath = "") {
|
|
84
|
-
const source = isRecord(schema) ? schema : {};
|
|
85
|
-
const segments = resolveSchemaPathSegments(schemaPath);
|
|
86
|
-
let current = source;
|
|
87
|
-
|
|
88
|
-
for (const segment of segments) {
|
|
89
|
-
if (Array.isArray(current)) {
|
|
90
|
-
const index = Number.parseInt(segment, 10);
|
|
91
|
-
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
current = current[index];
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!isRecord(current) || !Object.hasOwn(current, segment)) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
current = current[segment];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return current;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function collectSchemaFieldCandidates(schema = {}) {
|
|
108
|
-
if (!isRecord(schema)) {
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const fields = [];
|
|
113
|
-
const seenFields = new Set();
|
|
114
|
-
const addField = (value) => {
|
|
115
|
-
const normalized = normalizeText(value);
|
|
116
|
-
if (!normalized || seenFields.has(normalized)) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
seenFields.add(normalized);
|
|
121
|
-
fields.push(normalized);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
125
|
-
for (const field of Object.keys(properties)) {
|
|
126
|
-
addField(field);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
130
|
-
for (const field of required) {
|
|
131
|
-
addField(field);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const addFromSchema = (entry) => {
|
|
135
|
-
if (!isRecord(entry)) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const entryProperties = isRecord(entry.properties) ? entry.properties : {};
|
|
140
|
-
for (const field of Object.keys(entryProperties)) {
|
|
141
|
-
addField(field);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const entryRequired = Array.isArray(entry.required) ? entry.required : [];
|
|
145
|
-
for (const field of entryRequired) {
|
|
146
|
-
addField(field);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
for (const keyword of ["anyOf", "oneOf", "allOf"]) {
|
|
151
|
-
const branches = Array.isArray(schema[keyword]) ? schema[keyword] : [];
|
|
152
|
-
for (const branch of branches) {
|
|
153
|
-
addFromSchema(branch);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
for (const keyword of ["if", "then", "else"]) {
|
|
158
|
-
addFromSchema(schema[keyword]);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return fields;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function resolveConditionalIssueField(issue = {}, schema = {}) {
|
|
165
|
-
const params = isRecord(issue.params) ? issue.params : {};
|
|
166
|
-
const failingKeyword = normalizeText(params.failingKeyword).toLowerCase();
|
|
167
|
-
if (failingKeyword !== "then" && failingKeyword !== "else") {
|
|
168
|
-
return "";
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const scopeSchema = resolveSchemaNode(schema, issue.schemaPath) || schema;
|
|
172
|
-
if (!isRecord(scopeSchema)) {
|
|
173
|
-
return "";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const conditionalSchema = scopeSchema[failingKeyword];
|
|
177
|
-
const candidates = collectSchemaFieldCandidates(conditionalSchema);
|
|
178
|
-
return candidates[0] || "";
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function resolveFallbackIssueField(issue = {}, schema = {}) {
|
|
182
|
-
const fromSchemaPath = resolveFieldFromSchemaPath(issue.schemaPath);
|
|
183
|
-
if (fromSchemaPath) {
|
|
184
|
-
return fromSchemaPath;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const keyword = normalizeText(issue.keyword).toLowerCase();
|
|
188
|
-
if (keyword === "if") {
|
|
189
|
-
return resolveConditionalIssueField(issue, schema);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return "";
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function shouldSuppressRootUnionGlobalIssue(issue = {}, fieldErrors = {}) {
|
|
196
|
-
if (Object.keys(fieldErrors).length < 1) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const keyword = normalizeText(issue.keyword).toLowerCase();
|
|
201
|
-
if (keyword !== "anyof" && keyword !== "oneof" && keyword !== "allof") {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const instancePath = normalizeText(issue.instancePath);
|
|
206
|
-
const schemaPath = normalizeText(issue.schemaPath);
|
|
207
|
-
return !instancePath && (schemaPath === "#" || schemaPath === "#/");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function resolveMissingRequiredFields(issue = {}) {
|
|
211
|
-
const params = isRecord(issue.params) ? issue.params : {};
|
|
212
|
-
const requiredProperties = Array.isArray(params.requiredProperties)
|
|
213
|
-
? params.requiredProperties
|
|
214
|
-
: [];
|
|
215
|
-
|
|
216
|
-
return requiredProperties
|
|
217
|
-
.map((entry) => normalizeText(entry))
|
|
218
|
-
.filter(Boolean);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function resolveSchemaMessages(schema = {}) {
|
|
222
|
-
const source = isRecord(schema) ? schema : {};
|
|
223
|
-
const messages = source.messages;
|
|
224
|
-
return isRecord(messages) ? messages : {};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function resolveFieldSchema(schema = {}, field = "") {
|
|
228
|
-
const source = isRecord(schema) ? schema : {};
|
|
229
|
-
const properties = isRecord(source.properties) ? source.properties : {};
|
|
230
|
-
const fieldSchema = properties[field];
|
|
231
|
-
return isRecord(fieldSchema) ? fieldSchema : null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function resolveIssueMessageFromSchema(field, issue, schema = {}) {
|
|
235
|
-
const keyword = normalizeText(issue?.keyword);
|
|
236
|
-
|
|
237
|
-
if (field) {
|
|
238
|
-
const fieldSchema = resolveFieldSchema(schema, field);
|
|
239
|
-
const fieldMessages = resolveSchemaMessages(fieldSchema);
|
|
240
|
-
|
|
241
|
-
const fieldKeywordMessage = normalizeText(fieldMessages[keyword]);
|
|
242
|
-
if (fieldKeywordMessage) {
|
|
243
|
-
return fieldKeywordMessage;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const fieldDefaultMessage = normalizeText(fieldMessages.default);
|
|
247
|
-
if (fieldDefaultMessage) {
|
|
248
|
-
return fieldDefaultMessage;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const schemaMessages = resolveSchemaMessages(schema);
|
|
253
|
-
const schemaKeywordMessage = normalizeText(schemaMessages[keyword]);
|
|
254
|
-
if (schemaKeywordMessage) {
|
|
255
|
-
return schemaKeywordMessage;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const schemaDefaultMessage = normalizeText(schemaMessages.default);
|
|
259
|
-
if (schemaDefaultMessage) {
|
|
260
|
-
return schemaDefaultMessage;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const issueMessage = normalizeText(issue?.message);
|
|
264
|
-
if (issueMessage) {
|
|
265
|
-
return issueMessage;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return "Invalid value.";
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function mapOperationIssues(issues = [], schema = {}) {
|
|
272
|
-
const source = Array.isArray(issues) ? issues : [];
|
|
273
|
-
const fieldErrors = {};
|
|
274
|
-
const globalErrors = [];
|
|
275
|
-
|
|
276
|
-
for (const issue of source) {
|
|
277
|
-
const missingRequiredFields = resolveMissingRequiredFields(issue);
|
|
278
|
-
if (missingRequiredFields.length > 0) {
|
|
279
|
-
for (const field of missingRequiredFields) {
|
|
280
|
-
if (Object.hasOwn(fieldErrors, field)) {
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
fieldErrors[field] = resolveIssueMessageFromSchema(field, issue, schema);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const field = resolveIssueField(issue) || resolveFallbackIssueField(issue, schema);
|
|
291
|
-
if (field) {
|
|
292
|
-
if (!Object.hasOwn(fieldErrors, field)) {
|
|
293
|
-
fieldErrors[field] = resolveIssueMessageFromSchema(field, issue, schema);
|
|
294
|
-
}
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (shouldSuppressRootUnionGlobalIssue(issue, fieldErrors)) {
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
globalErrors.push(resolveIssueMessageFromSchema("", issue, schema));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
fieldErrors,
|
|
307
|
-
globalErrors
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export {
|
|
312
|
-
resolveSchemaMessages,
|
|
313
|
-
resolveFieldSchema,
|
|
314
|
-
resolveIssueField,
|
|
315
|
-
resolveMissingRequiredFields,
|
|
316
|
-
resolveIssueMessageFromSchema,
|
|
317
|
-
mapOperationIssues
|
|
318
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createSchema } from "json-rest-schema";
|
|
2
|
-
|
|
3
|
-
function createPaginationQuerySchema({
|
|
4
|
-
defaultPage = 1,
|
|
5
|
-
defaultPageSize = 10,
|
|
6
|
-
minPage = 1,
|
|
7
|
-
minPageSize = 1,
|
|
8
|
-
maxPageSize = 100
|
|
9
|
-
} = {}) {
|
|
10
|
-
return createSchema({
|
|
11
|
-
page: {
|
|
12
|
-
type: "integer",
|
|
13
|
-
required: false,
|
|
14
|
-
min: minPage,
|
|
15
|
-
default: defaultPage
|
|
16
|
-
},
|
|
17
|
-
pageSize: {
|
|
18
|
-
type: "integer",
|
|
19
|
-
required: false,
|
|
20
|
-
min: minPageSize,
|
|
21
|
-
max: maxPageSize,
|
|
22
|
-
default: defaultPageSize
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export { createPaginationQuerySchema };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { asSchemaDefinition } from "./schemaUtils.js";
|
|
2
|
-
import { createCursorListValidator } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
-
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
4
|
-
|
|
5
|
-
function createCursorPagedListResponseSchema(itemSchema) {
|
|
6
|
-
return createCursorListValidator(itemSchema);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function createResource({
|
|
10
|
-
record,
|
|
11
|
-
create,
|
|
12
|
-
replace,
|
|
13
|
-
patch,
|
|
14
|
-
list = null,
|
|
15
|
-
listItem = null
|
|
16
|
-
} = {}) {
|
|
17
|
-
const normalizedRecordSchema = asSchemaDefinition(record, "record", "replace");
|
|
18
|
-
const normalizedCreateSchema = asSchemaDefinition(create, "create", "create");
|
|
19
|
-
const normalizedReplaceSchema = asSchemaDefinition(replace, "replace", "replace");
|
|
20
|
-
const normalizedPatchSchema = asSchemaDefinition(patch, "patch", "patch");
|
|
21
|
-
const normalizedListItemSchema = listItem
|
|
22
|
-
? asSchemaDefinition(listItem, "listItem", "replace")
|
|
23
|
-
: normalizedRecordSchema;
|
|
24
|
-
const normalizedListSchema = list
|
|
25
|
-
? asSchemaDefinition(list, "list", "replace")
|
|
26
|
-
: createCursorPagedListResponseSchema(normalizedListItemSchema);
|
|
27
|
-
|
|
28
|
-
return deepFreeze({
|
|
29
|
-
record: normalizedRecordSchema,
|
|
30
|
-
create: normalizedCreateSchema,
|
|
31
|
-
replace: normalizedReplaceSchema,
|
|
32
|
-
patch: normalizedPatchSchema,
|
|
33
|
-
listItem: normalizedListItemSchema,
|
|
34
|
-
list: normalizedListSchema
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export {
|
|
39
|
-
createCursorPagedListResponseSchema,
|
|
40
|
-
createResource
|
|
41
|
-
};
|
package/test/command.test.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { createSchema } from "json-rest-schema";
|
|
4
|
-
import { createCommand } from "../src/shared/validators/command.js";
|
|
5
|
-
|
|
6
|
-
test("createCommand requires input/output schema definitions", () => {
|
|
7
|
-
assert.throws(() => createCommand({}), /input is required/);
|
|
8
|
-
|
|
9
|
-
assert.throws(
|
|
10
|
-
() =>
|
|
11
|
-
createCommand({
|
|
12
|
-
input: {
|
|
13
|
-
schema: createSchema({})
|
|
14
|
-
}
|
|
15
|
-
}),
|
|
16
|
-
/output is required/
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
assert.throws(
|
|
20
|
-
() =>
|
|
21
|
-
createCommand({
|
|
22
|
-
input: createSchema({}),
|
|
23
|
-
output: {
|
|
24
|
-
schema: createSchema({})
|
|
25
|
-
}
|
|
26
|
-
}),
|
|
27
|
-
/input must be a schema definition object/
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("createCommand normalizes invalidates and preserves idempotent flag", () => {
|
|
32
|
-
const command = createCommand({
|
|
33
|
-
input: {
|
|
34
|
-
schema: createSchema({
|
|
35
|
-
token: { type: "string", required: true, minLength: 1 }
|
|
36
|
-
})
|
|
37
|
-
},
|
|
38
|
-
output: {
|
|
39
|
-
schema: createSchema({
|
|
40
|
-
ok: { type: "boolean", required: true }
|
|
41
|
-
})
|
|
42
|
-
},
|
|
43
|
-
idempotent: true,
|
|
44
|
-
invalidates: ["users-web", "users-web", "", "workspace.members"]
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
assert.equal(command.idempotent, true);
|
|
48
|
-
assert.deepEqual(command.invalidates, ["users-web", "workspace.members"]);
|
|
49
|
-
assert.equal(command.input.mode, "patch");
|
|
50
|
-
assert.equal(command.output.mode, "replace");
|
|
51
|
-
assert.equal(typeof command.input.schema.toJsonSchema, "function");
|
|
52
|
-
assert.equal(typeof command.output.schema.toJsonSchema, "function");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("createCommand omits idempotent when not explicitly boolean", () => {
|
|
56
|
-
const command = createCommand({
|
|
57
|
-
input: {
|
|
58
|
-
schema: createSchema({})
|
|
59
|
-
},
|
|
60
|
-
output: {
|
|
61
|
-
schema: createSchema({})
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
assert.equal(Object.hasOwn(command, "idempotent"), false);
|
|
66
|
-
});
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
mapOperationIssues,
|
|
5
|
-
resolveFieldSchema,
|
|
6
|
-
resolveIssueField,
|
|
7
|
-
resolveMissingRequiredFields,
|
|
8
|
-
resolveSchemaMessages
|
|
9
|
-
} from "../src/shared/validators/operationMessages.js";
|
|
10
|
-
|
|
11
|
-
const sampleSchema = {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: {
|
|
14
|
-
name: {
|
|
15
|
-
type: "string",
|
|
16
|
-
minLength: 1,
|
|
17
|
-
messages: {
|
|
18
|
-
required: "Workspace name is required.",
|
|
19
|
-
minLength: "Workspace name is required.",
|
|
20
|
-
default: "Invalid workspace name."
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
color: {
|
|
24
|
-
type: "string",
|
|
25
|
-
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
26
|
-
messages: {
|
|
27
|
-
pattern: "Workspace color must be a hex value."
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
invitesEnabled: {
|
|
31
|
-
type: "boolean",
|
|
32
|
-
messages: {
|
|
33
|
-
default: "invitesEnabled must be true or false."
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
additionalProperties: false,
|
|
38
|
-
messages: {
|
|
39
|
-
additionalProperties: "Unexpected field."
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
test("resolveIssueField resolves missing and nested fields", () => {
|
|
44
|
-
const requiredIssue = {
|
|
45
|
-
keyword: "required",
|
|
46
|
-
params: {
|
|
47
|
-
missingProperty: "name",
|
|
48
|
-
requiredProperties: ["name"]
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
assert.equal(resolveIssueField(requiredIssue), "name");
|
|
53
|
-
assert.deepEqual(resolveMissingRequiredFields(requiredIssue), ["name"]);
|
|
54
|
-
|
|
55
|
-
const minLengthIssue = {
|
|
56
|
-
keyword: "minLength",
|
|
57
|
-
instancePath: "/profile/displayName"
|
|
58
|
-
};
|
|
59
|
-
assert.equal(resolveIssueField(minLengthIssue), "profile");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("mapOperationIssues applies field message overrides by keyword", () => {
|
|
63
|
-
const issues = [
|
|
64
|
-
{
|
|
65
|
-
keyword: "minLength",
|
|
66
|
-
instancePath: "/name"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
keyword: "pattern",
|
|
70
|
-
instancePath: "/color"
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
keyword: "type",
|
|
74
|
-
instancePath: "/invitesEnabled"
|
|
75
|
-
}
|
|
76
|
-
];
|
|
77
|
-
const mapped = mapOperationIssues(issues, sampleSchema);
|
|
78
|
-
|
|
79
|
-
assert.equal(mapped.fieldErrors.name, "Workspace name is required.");
|
|
80
|
-
assert.equal(mapped.fieldErrors.color, "Workspace color must be a hex value.");
|
|
81
|
-
assert.equal(mapped.fieldErrors.invitesEnabled, "invitesEnabled must be true or false.");
|
|
82
|
-
assert.deepEqual(mapped.globalErrors, []);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("mapOperationIssues falls back to keyword/global messages", () => {
|
|
86
|
-
const issues = [
|
|
87
|
-
{
|
|
88
|
-
keyword: "additionalProperties",
|
|
89
|
-
params: {
|
|
90
|
-
additionalProperty: "extra"
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
];
|
|
94
|
-
const mapped = mapOperationIssues(issues, sampleSchema);
|
|
95
|
-
|
|
96
|
-
assert.equal(mapped.fieldErrors.extra, "Unexpected field.");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("mapOperationIssues maps conditional schema failures to field errors", () => {
|
|
100
|
-
const conditionalSchema = {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: {
|
|
103
|
-
isVaccinated: {
|
|
104
|
-
type: "boolean"
|
|
105
|
-
},
|
|
106
|
-
adenovirusValidTo: {
|
|
107
|
-
type: "string"
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
if: {
|
|
111
|
-
properties: {
|
|
112
|
-
isVaccinated: {
|
|
113
|
-
const: true
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
then: {
|
|
118
|
-
required: ["adenovirusValidTo"]
|
|
119
|
-
},
|
|
120
|
-
messages: {
|
|
121
|
-
if: "Adenovirus valid-to date is required when vaccinated."
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const issues = [
|
|
126
|
-
{
|
|
127
|
-
keyword: "if",
|
|
128
|
-
schemaPath: "#",
|
|
129
|
-
params: {
|
|
130
|
-
failingKeyword: "then"
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
];
|
|
134
|
-
const mapped = mapOperationIssues(issues, conditionalSchema);
|
|
135
|
-
|
|
136
|
-
assert.equal(mapped.fieldErrors.adenovirusValidTo, "Adenovirus valid-to date is required when vaccinated.");
|
|
137
|
-
assert.deepEqual(mapped.globalErrors, []);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("mapOperationIssues suppresses redundant root anyOf global issue when field errors exist", () => {
|
|
141
|
-
const unionSchema = {
|
|
142
|
-
anyOf: [
|
|
143
|
-
{
|
|
144
|
-
type: "object",
|
|
145
|
-
properties: {
|
|
146
|
-
kind: {
|
|
147
|
-
const: "dog"
|
|
148
|
-
},
|
|
149
|
-
bark: {
|
|
150
|
-
type: "string",
|
|
151
|
-
minLength: 1
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
additionalProperties: false
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
type: "object",
|
|
158
|
-
properties: {
|
|
159
|
-
kind: {
|
|
160
|
-
const: "cat"
|
|
161
|
-
},
|
|
162
|
-
meow: {
|
|
163
|
-
type: "string",
|
|
164
|
-
minLength: 1
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
additionalProperties: false
|
|
168
|
-
}
|
|
169
|
-
]
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const issues = [
|
|
173
|
-
{
|
|
174
|
-
keyword: "minLength",
|
|
175
|
-
instancePath: "/bark"
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
keyword: "anyOf",
|
|
179
|
-
schemaPath: "#"
|
|
180
|
-
}
|
|
181
|
-
];
|
|
182
|
-
const mapped = mapOperationIssues(issues, unionSchema);
|
|
183
|
-
|
|
184
|
-
assert.equal(typeof mapped.fieldErrors.bark, "string");
|
|
185
|
-
assert.deepEqual(mapped.globalErrors, []);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("schema message helpers resolve field and root messages", () => {
|
|
189
|
-
const nameSchema = resolveFieldSchema(sampleSchema, "name");
|
|
190
|
-
assert.equal(typeof nameSchema, "object");
|
|
191
|
-
|
|
192
|
-
const nameMessages = resolveSchemaMessages(nameSchema);
|
|
193
|
-
const rootMessages = resolveSchemaMessages(sampleSchema);
|
|
194
|
-
|
|
195
|
-
assert.equal(nameMessages.minLength, "Workspace name is required.");
|
|
196
|
-
assert.equal(rootMessages.additionalProperties, "Unexpected field.");
|
|
197
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
import { createPaginationQuerySchema } from "../src/shared/validators/paginationQuery.js";
|
|
5
|
-
|
|
6
|
-
test("createPaginationQuerySchema exports the expected transport bounds", () => {
|
|
7
|
-
const schema = createPaginationQuerySchema().toJsonSchema({ mode: "patch" });
|
|
8
|
-
|
|
9
|
-
assert.equal(schema.type, "object");
|
|
10
|
-
assert.equal(schema.additionalProperties, false);
|
|
11
|
-
assert.equal(schema.properties.page.minimum, 1);
|
|
12
|
-
assert.equal(schema.properties.pageSize.minimum, 1);
|
|
13
|
-
assert.equal(schema.properties.pageSize.maximum, 100);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("createPaginationQuerySchema applies custom transport bounds", () => {
|
|
17
|
-
const schema = createPaginationQuerySchema({
|
|
18
|
-
defaultPage: 2,
|
|
19
|
-
defaultPageSize: 25,
|
|
20
|
-
minPage: 2,
|
|
21
|
-
minPageSize: 5,
|
|
22
|
-
maxPageSize: 250
|
|
23
|
-
}).toJsonSchema({ mode: "patch" });
|
|
24
|
-
|
|
25
|
-
assert.equal(schema.properties.page.minimum, 2);
|
|
26
|
-
assert.equal(schema.properties.pageSize.minimum, 5);
|
|
27
|
-
assert.equal(schema.properties.pageSize.maximum, 250);
|
|
28
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
5
|
-
import { HttpClientProvider } from "../src/server/providers/HttpClientProvider.js";
|
|
6
|
-
|
|
7
|
-
test("HttpClientProvider provides the server HTTP client API", async () => {
|
|
8
|
-
let httpClient;
|
|
9
|
-
const consumer = defineProvider({
|
|
10
|
-
id: "test.http-client.consumer",
|
|
11
|
-
requires: { value: "runtime.http-client" },
|
|
12
|
-
setup({ value }) {
|
|
13
|
-
httpClient = value;
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
const runtime = createCapabilityRuntime({ providers: [HttpClientProvider, consumer] });
|
|
18
|
-
await runtime.start();
|
|
19
|
-
assert.equal(typeof httpClient.createHttpClient, "function");
|
|
20
|
-
assert.equal(typeof httpClient.shouldRetryForCsrfFailure, "function");
|
|
21
|
-
await runtime.shutdown();
|
|
22
|
-
});
|