@jskit-ai/http-runtime 0.1.54 → 0.1.55
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.descriptor.mjs +2 -4
- package/package.json +5 -5
- package/src/shared/clientRuntime/client.js +126 -9
- package/src/shared/clientRuntime/errors.js +6 -0
- package/src/shared/clientRuntime/jsonApiResourceTransport.js +241 -0
- package/src/shared/index.js +54 -5
- package/src/shared/validators/command.js +5 -4
- package/src/shared/validators/errorResponses.js +125 -62
- package/src/shared/validators/httpValidatorsApi.js +83 -12
- package/src/shared/validators/jsonApiQueryTransport.js +211 -0
- package/src/shared/validators/jsonApiResponses.js +3 -0
- package/src/shared/validators/jsonApiResult.js +83 -0
- package/src/shared/validators/jsonApiRouteTransport.js +800 -0
- package/src/shared/validators/jsonApiTransport.js +484 -0
- package/src/shared/validators/operationValidation.js +62 -101
- package/src/shared/validators/paginationQuery.js +14 -19
- package/src/shared/validators/resource.js +15 -17
- package/src/shared/validators/schemaUtils.js +18 -5
- package/src/shared/validators/transportSchemaEmbedding.js +81 -0
- package/test/client.test.js +279 -0
- package/test/command.test.js +38 -21
- package/test/entrypoints.boundary.test.js +8 -0
- package/test/errorResponses.test.js +49 -13
- package/test/jsonApiRouteTransport.test.js +349 -0
- package/test/jsonApiTransport.test.js +231 -0
- package/test/operationMessages.test.js +115 -66
- package/test/operationValidation.test.js +147 -159
- package/test/paginationQuery.test.js +4 -8
- package/test/resource.test.js +89 -55
- package/test/validationErrors.test.js +33 -0
- package/src/shared/validators/typeboxFormats.js +0 -43
- package/test/typeboxFormats.test.js +0 -42
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { normalizeArray, normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
|
|
4
|
+
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeMediaType(contentType = "") {
|
|
10
|
+
return String(contentType || "")
|
|
11
|
+
.split(";")[0]
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isJsonContentType(contentType = "") {
|
|
17
|
+
const mediaType = normalizeMediaType(contentType);
|
|
18
|
+
if (!mediaType) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return mediaType === "application/json" || /^application\/[a-z0-9.!#$&^_-]+\+json$/i.test(mediaType);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isJsonApiContentType(contentType = "") {
|
|
26
|
+
return normalizeMediaType(contentType) === JSON_API_CONTENT_TYPE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveJsonApiTransportTypes({
|
|
30
|
+
type = "",
|
|
31
|
+
requestType = "",
|
|
32
|
+
responseType = ""
|
|
33
|
+
} = {}, {
|
|
34
|
+
context = "JSON:API transport"
|
|
35
|
+
} = {}) {
|
|
36
|
+
const fallbackType = normalizeText(type);
|
|
37
|
+
const normalizedRequestType = normalizeText(requestType, {
|
|
38
|
+
fallback: fallbackType
|
|
39
|
+
});
|
|
40
|
+
const normalizedResponseType = normalizeText(responseType, {
|
|
41
|
+
fallback: fallbackType || normalizedRequestType
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!normalizedRequestType && !normalizedResponseType) {
|
|
45
|
+
throw new TypeError(`${context} requires requestType, responseType, or type.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
requestType: normalizedRequestType,
|
|
50
|
+
responseType: normalizedResponseType
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeJsonApiResourceObject(resource = {}) {
|
|
55
|
+
if (!isRecord(resource)) {
|
|
56
|
+
return Object.freeze({});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const source = normalizeObject(resource);
|
|
60
|
+
const normalized = {};
|
|
61
|
+
|
|
62
|
+
if (source.type != null) {
|
|
63
|
+
normalized.type = String(source.type);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (source.id != null) {
|
|
67
|
+
normalized.id = String(source.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isRecord(source.attributes)) {
|
|
71
|
+
normalized.attributes = normalizeObject(source.attributes);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isRecord(source.relationships)) {
|
|
75
|
+
normalized.relationships = normalizeObject(source.relationships);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isRecord(source.links)) {
|
|
79
|
+
normalized.links = normalizeObject(source.links);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isRecord(source.meta)) {
|
|
83
|
+
normalized.meta = normalizeObject(source.meta);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Object.freeze(normalized);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeJsonApiResourceArray(value) {
|
|
90
|
+
return Object.freeze(normalizeArray(value).map((entry) => normalizeJsonApiResourceObject(entry)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeJsonApiLinks(value) {
|
|
94
|
+
return isRecord(value) ? Object.freeze(normalizeObject(value)) : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeJsonApiMeta(value) {
|
|
98
|
+
return isRecord(value) ? Object.freeze(normalizeObject(value)) : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeJsonApiErrors(value) {
|
|
102
|
+
return Array.isArray(value)
|
|
103
|
+
? Object.freeze(value.map((entry) => (isRecord(entry) ? normalizeObject(entry) : {})))
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isJsonApiResourceDocument(payload = {}) {
|
|
108
|
+
return isRecord(payload) && isRecord(payload.data) && !Array.isArray(payload.data);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isJsonApiCollectionDocument(payload = {}) {
|
|
112
|
+
return isRecord(payload) && Array.isArray(payload.data);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isJsonApiErrorDocument(payload = {}) {
|
|
116
|
+
return isRecord(payload) && Array.isArray(payload.errors);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeJsonApiDocument(payload = {}) {
|
|
120
|
+
const source = isRecord(payload) ? normalizeObject(payload) : {};
|
|
121
|
+
const links = normalizeJsonApiLinks(source.links);
|
|
122
|
+
const meta = normalizeJsonApiMeta(source.meta);
|
|
123
|
+
const included = normalizeJsonApiResourceArray(source.included);
|
|
124
|
+
|
|
125
|
+
if (isJsonApiResourceDocument(source)) {
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
kind: "resource",
|
|
128
|
+
data: normalizeJsonApiResourceObject(source.data),
|
|
129
|
+
...(included.length > 0 ? { included } : {}),
|
|
130
|
+
...(links ? { links } : {}),
|
|
131
|
+
...(meta ? { meta } : {})
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Object.hasOwn(source, "data") && source.data == null) {
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
kind: "resource",
|
|
138
|
+
data: null,
|
|
139
|
+
...(included.length > 0 ? { included } : {}),
|
|
140
|
+
...(links ? { links } : {}),
|
|
141
|
+
...(meta ? { meta } : {})
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isJsonApiCollectionDocument(source)) {
|
|
146
|
+
return Object.freeze({
|
|
147
|
+
kind: "collection",
|
|
148
|
+
data: normalizeJsonApiResourceArray(source.data),
|
|
149
|
+
...(included.length > 0 ? { included } : {}),
|
|
150
|
+
...(links ? { links } : {}),
|
|
151
|
+
...(meta ? { meta } : {})
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (isJsonApiErrorDocument(source)) {
|
|
156
|
+
const errors = normalizeJsonApiErrors(source.errors);
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
kind: "errors",
|
|
159
|
+
errors: errors || Object.freeze([]),
|
|
160
|
+
...(links ? { links } : {}),
|
|
161
|
+
...(meta ? { meta } : {})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (meta && !Object.hasOwn(source, "data") && !Object.hasOwn(source, "errors")) {
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
kind: "meta",
|
|
168
|
+
meta,
|
|
169
|
+
...(links ? { links } : {})
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
kind: "unknown",
|
|
175
|
+
value: source
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createJsonApiResourceObject({
|
|
180
|
+
type = "",
|
|
181
|
+
id = null,
|
|
182
|
+
attributes = undefined,
|
|
183
|
+
relationships = undefined,
|
|
184
|
+
links = undefined,
|
|
185
|
+
meta = undefined
|
|
186
|
+
} = {}) {
|
|
187
|
+
const normalizedType = String(type || "").trim();
|
|
188
|
+
if (!normalizedType) {
|
|
189
|
+
throw new TypeError("createJsonApiResourceObject requires a non-empty type.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const resource = {
|
|
193
|
+
type: normalizedType
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (id != null && String(id).trim()) {
|
|
197
|
+
resource.id = String(id);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (attributes !== undefined) {
|
|
201
|
+
if (!isRecord(attributes)) {
|
|
202
|
+
throw new TypeError("createJsonApiResourceObject attributes must be an object.");
|
|
203
|
+
}
|
|
204
|
+
resource.attributes = normalizeObject(attributes);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (relationships !== undefined) {
|
|
208
|
+
if (!isRecord(relationships)) {
|
|
209
|
+
throw new TypeError("createJsonApiResourceObject relationships must be an object.");
|
|
210
|
+
}
|
|
211
|
+
resource.relationships = normalizeObject(relationships);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (links !== undefined) {
|
|
215
|
+
if (!isRecord(links)) {
|
|
216
|
+
throw new TypeError("createJsonApiResourceObject links must be an object.");
|
|
217
|
+
}
|
|
218
|
+
resource.links = normalizeObject(links);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (meta !== undefined) {
|
|
222
|
+
if (!isRecord(meta)) {
|
|
223
|
+
throw new TypeError("createJsonApiResourceObject meta must be an object.");
|
|
224
|
+
}
|
|
225
|
+
resource.meta = normalizeObject(meta);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return Object.freeze(resource);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createJsonApiDocument({
|
|
232
|
+
data = undefined,
|
|
233
|
+
included = undefined,
|
|
234
|
+
links = undefined,
|
|
235
|
+
meta = undefined,
|
|
236
|
+
errors = undefined
|
|
237
|
+
} = {}) {
|
|
238
|
+
const hasData = data !== undefined;
|
|
239
|
+
const hasErrors = errors !== undefined;
|
|
240
|
+
const hasMeta = meta !== undefined;
|
|
241
|
+
|
|
242
|
+
if (!hasData && !hasErrors && !hasMeta) {
|
|
243
|
+
throw new TypeError("createJsonApiDocument requires at least one of data, errors, or meta.");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (hasData && hasErrors) {
|
|
247
|
+
throw new TypeError("createJsonApiDocument cannot include both data and errors.");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const document = {};
|
|
251
|
+
|
|
252
|
+
if (hasData) {
|
|
253
|
+
if (data == null) {
|
|
254
|
+
document.data = null;
|
|
255
|
+
} else if (Array.isArray(data)) {
|
|
256
|
+
document.data = normalizeJsonApiResourceArray(data);
|
|
257
|
+
} else if (isRecord(data)) {
|
|
258
|
+
document.data = normalizeJsonApiResourceObject(data);
|
|
259
|
+
} else {
|
|
260
|
+
throw new TypeError("createJsonApiDocument data must be a resource object, an array, or null.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (hasErrors) {
|
|
265
|
+
if (!Array.isArray(errors)) {
|
|
266
|
+
throw new TypeError("createJsonApiDocument errors must be an array.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
document.errors = normalizeJsonApiErrors(errors) || Object.freeze([]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (included !== undefined) {
|
|
273
|
+
if (!Array.isArray(included)) {
|
|
274
|
+
throw new TypeError("createJsonApiDocument included must be an array.");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
document.included = normalizeJsonApiResourceArray(included);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (links !== undefined) {
|
|
281
|
+
if (!isRecord(links)) {
|
|
282
|
+
throw new TypeError("createJsonApiDocument links must be an object.");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
document.links = Object.freeze(normalizeObject(links));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (meta !== undefined) {
|
|
289
|
+
if (!isRecord(meta)) {
|
|
290
|
+
throw new TypeError("createJsonApiDocument meta must be an object.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
document.meta = Object.freeze(normalizeObject(meta));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return Object.freeze(document);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function createJsonApiErrorObject({
|
|
300
|
+
status = "",
|
|
301
|
+
code = "",
|
|
302
|
+
title = "",
|
|
303
|
+
detail = "",
|
|
304
|
+
source = undefined,
|
|
305
|
+
links = undefined,
|
|
306
|
+
meta = undefined
|
|
307
|
+
} = {}) {
|
|
308
|
+
const errorObject = {};
|
|
309
|
+
const normalizedStatus = String(status || "").trim();
|
|
310
|
+
const normalizedCode = String(code || "").trim();
|
|
311
|
+
const normalizedTitle = String(title || "").trim();
|
|
312
|
+
const normalizedDetail = String(detail || "").trim();
|
|
313
|
+
|
|
314
|
+
if (normalizedStatus) {
|
|
315
|
+
errorObject.status = normalizedStatus;
|
|
316
|
+
}
|
|
317
|
+
if (normalizedCode) {
|
|
318
|
+
errorObject.code = normalizedCode;
|
|
319
|
+
}
|
|
320
|
+
if (normalizedTitle) {
|
|
321
|
+
errorObject.title = normalizedTitle;
|
|
322
|
+
}
|
|
323
|
+
if (normalizedDetail) {
|
|
324
|
+
errorObject.detail = normalizedDetail;
|
|
325
|
+
}
|
|
326
|
+
if (source !== undefined) {
|
|
327
|
+
if (!isRecord(source)) {
|
|
328
|
+
throw new TypeError("createJsonApiErrorObject source must be an object.");
|
|
329
|
+
}
|
|
330
|
+
errorObject.source = normalizeObject(source);
|
|
331
|
+
}
|
|
332
|
+
if (links !== undefined) {
|
|
333
|
+
if (!isRecord(links)) {
|
|
334
|
+
throw new TypeError("createJsonApiErrorObject links must be an object.");
|
|
335
|
+
}
|
|
336
|
+
errorObject.links = normalizeObject(links);
|
|
337
|
+
}
|
|
338
|
+
if (meta !== undefined) {
|
|
339
|
+
if (!isRecord(meta)) {
|
|
340
|
+
throw new TypeError("createJsonApiErrorObject meta must be an object.");
|
|
341
|
+
}
|
|
342
|
+
errorObject.meta = normalizeObject(meta);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return Object.freeze(errorObject);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function escapeJsonPointerSegment(value = "") {
|
|
349
|
+
return String(value || "")
|
|
350
|
+
.replace(/~/g, "~0")
|
|
351
|
+
.replace(/\//g, "~1");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function resolveValidationFieldName(issue = {}) {
|
|
355
|
+
const pathParts = String(issue?.instancePath || "")
|
|
356
|
+
.split("/")
|
|
357
|
+
.filter(Boolean);
|
|
358
|
+
const missingProperty = String(issue?.params?.missingProperty || "").trim();
|
|
359
|
+
const additionalProperty = String(issue?.params?.additionalProperty || "").trim();
|
|
360
|
+
const leaf = additionalProperty || missingProperty || pathParts[pathParts.length - 1] || "";
|
|
361
|
+
return leaf;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resolveJsonApiValidationSource(issue = {}, validationContext = "") {
|
|
365
|
+
const normalizedContext = String(validationContext || "").trim().toLowerCase();
|
|
366
|
+
const fieldName = resolveValidationFieldName(issue);
|
|
367
|
+
if (normalizedContext === "querystring" || normalizedContext === "params") {
|
|
368
|
+
return fieldName ? Object.freeze({ parameter: fieldName }) : undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const instancePath = String(issue?.instancePath || "").trim();
|
|
372
|
+
if (!instancePath && !fieldName) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const pointerBase = instancePath || "";
|
|
377
|
+
const pointer =
|
|
378
|
+
fieldName && !pointerBase.endsWith(`/${fieldName}`)
|
|
379
|
+
? `${pointerBase}/${escapeJsonPointerSegment(fieldName)}`
|
|
380
|
+
: pointerBase;
|
|
381
|
+
return Object.freeze({
|
|
382
|
+
pointer: pointer || "/"
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function createJsonApiErrorDocumentFromFailure({
|
|
387
|
+
statusCode = 500,
|
|
388
|
+
code = "",
|
|
389
|
+
message = "",
|
|
390
|
+
fieldErrors = {},
|
|
391
|
+
validationIssues = [],
|
|
392
|
+
validationContext = "",
|
|
393
|
+
pointerPrefix = "/data/attributes"
|
|
394
|
+
} = {}) {
|
|
395
|
+
const normalizedStatus = String(Number(statusCode) || 500);
|
|
396
|
+
const normalizedCode = String(code || "").trim();
|
|
397
|
+
const normalizedMessage = String(message || "").trim() || "Request failed.";
|
|
398
|
+
const normalizedFieldErrors = isRecord(fieldErrors) ? normalizeObject(fieldErrors) : {};
|
|
399
|
+
const issues = Array.isArray(validationIssues) ? validationIssues : [];
|
|
400
|
+
|
|
401
|
+
if (issues.length > 0) {
|
|
402
|
+
return createJsonApiDocument({
|
|
403
|
+
errors: issues.map((issue) =>
|
|
404
|
+
createJsonApiErrorObject({
|
|
405
|
+
status: normalizedStatus,
|
|
406
|
+
code: normalizedCode,
|
|
407
|
+
title: normalizedMessage,
|
|
408
|
+
detail: String(issue?.message || "Invalid value.").trim() || "Invalid value.",
|
|
409
|
+
source: resolveJsonApiValidationSource(issue, validationContext)
|
|
410
|
+
})
|
|
411
|
+
)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const fieldEntries = Object.entries(normalizedFieldErrors).filter(([field]) => String(field || "").trim());
|
|
416
|
+
if (fieldEntries.length > 0) {
|
|
417
|
+
return createJsonApiDocument({
|
|
418
|
+
errors: fieldEntries.map(([field, detail]) =>
|
|
419
|
+
createJsonApiErrorObject({
|
|
420
|
+
status: normalizedStatus,
|
|
421
|
+
code: normalizedCode,
|
|
422
|
+
title: normalizedMessage,
|
|
423
|
+
detail: String(detail || "Invalid value.").trim() || "Invalid value.",
|
|
424
|
+
source: {
|
|
425
|
+
pointer: `${pointerPrefix}/${escapeJsonPointerSegment(field)}`
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return createJsonApiDocument({
|
|
433
|
+
errors: [
|
|
434
|
+
createJsonApiErrorObject({
|
|
435
|
+
status: normalizedStatus,
|
|
436
|
+
code: normalizedCode,
|
|
437
|
+
title: normalizedMessage
|
|
438
|
+
})
|
|
439
|
+
]
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function simplifyJsonApiResourceObject(resource = {}) {
|
|
444
|
+
const normalized = normalizeJsonApiResourceObject(resource);
|
|
445
|
+
return {
|
|
446
|
+
id: normalized.id == null ? "" : normalized.id,
|
|
447
|
+
...(normalized.attributes || {})
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function simplifyJsonApiDocument(payload = {}) {
|
|
452
|
+
const document = normalizeJsonApiDocument(payload);
|
|
453
|
+
|
|
454
|
+
if (document.kind === "resource") {
|
|
455
|
+
return document.data == null ? null : simplifyJsonApiResourceObject(document.data);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (document.kind === "collection") {
|
|
459
|
+
return document.data.map((entry) => simplifyJsonApiResourceObject(entry));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (document.kind === "meta") {
|
|
463
|
+
return document.meta || {};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return payload;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export {
|
|
470
|
+
JSON_API_CONTENT_TYPE,
|
|
471
|
+
resolveJsonApiTransportTypes,
|
|
472
|
+
createJsonApiDocument,
|
|
473
|
+
createJsonApiErrorDocumentFromFailure,
|
|
474
|
+
createJsonApiErrorObject,
|
|
475
|
+
createJsonApiResourceObject,
|
|
476
|
+
isJsonApiCollectionDocument,
|
|
477
|
+
isJsonApiContentType,
|
|
478
|
+
isJsonApiErrorDocument,
|
|
479
|
+
isJsonApiResourceDocument,
|
|
480
|
+
isJsonContentType,
|
|
481
|
+
normalizeJsonApiDocument,
|
|
482
|
+
normalizeJsonApiResourceObject,
|
|
483
|
+
simplifyJsonApiDocument
|
|
484
|
+
};
|
|
@@ -1,107 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mapOperationIssues } from "./operationMessages.js";
|
|
3
|
-
import { resolveFieldErrors } from "../support/fieldErrors.js";
|
|
4
|
-
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
1
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
5
2
|
|
|
6
|
-
function
|
|
7
|
-
if (!
|
|
8
|
-
return
|
|
3
|
+
function resolveOperationSection(operation = {}, section = "body") {
|
|
4
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
5
|
+
return null;
|
|
9
6
|
}
|
|
10
7
|
|
|
8
|
+
return operation[section] == null ? null : operation[section];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildValidationSuccessResult(value) {
|
|
11
12
|
return {
|
|
12
|
-
|
|
13
|
+
ok: true,
|
|
14
|
+
value,
|
|
15
|
+
normalized: value,
|
|
16
|
+
fieldErrors: {},
|
|
17
|
+
globalErrors: [],
|
|
18
|
+
issues: []
|
|
13
19
|
};
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!isRecord(value)) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
+
function isFieldValidationError(error) {
|
|
23
|
+
return Boolean(error?.fieldErrors && typeof error.fieldErrors === "object");
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
function buildValidationFailureResult(error, normalized) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
value: null,
|
|
30
|
+
normalized,
|
|
31
|
+
fieldErrors: error.fieldErrors,
|
|
32
|
+
globalErrors: [],
|
|
33
|
+
issues: []
|
|
34
|
+
};
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function validateOperationSection({
|
|
27
38
|
operation = {},
|
|
28
|
-
section = "
|
|
39
|
+
section = "body",
|
|
29
40
|
value,
|
|
30
41
|
context = {}
|
|
31
42
|
} = {}) {
|
|
32
43
|
const sectionDefinition = resolveOperationSection(operation, section);
|
|
33
44
|
if (!sectionDefinition) {
|
|
34
|
-
return
|
|
35
|
-
ok: true,
|
|
36
|
-
value,
|
|
37
|
-
normalized: value,
|
|
38
|
-
fieldErrors: {},
|
|
39
|
-
globalErrors: [],
|
|
40
|
-
issues: []
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const schema = sectionDefinition.schema;
|
|
45
|
-
if (!isRecord(schema)) {
|
|
46
|
-
throw new TypeError(`Operation section \"${section}\" requires a schema object.`);
|
|
45
|
+
return buildValidationSuccessResult(value);
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
const normalize = typeof sectionDefinition.normalize === "function"
|
|
50
|
-
? sectionDefinition.normalize
|
|
51
|
-
: defaultNormalize;
|
|
52
|
-
|
|
53
|
-
let normalized = null;
|
|
54
48
|
try {
|
|
55
|
-
normalized =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
value: null,
|
|
62
|
-
normalized: value,
|
|
63
|
-
fieldErrors: explicitFieldErrors,
|
|
64
|
-
globalErrors: [],
|
|
65
|
-
issues: []
|
|
66
|
-
};
|
|
67
|
-
}
|
|
49
|
+
const normalized = validateSchemaPayload(sectionDefinition, value, {
|
|
50
|
+
phase: "input",
|
|
51
|
+
context: `operation section "${section}"`
|
|
52
|
+
});
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
value: null,
|
|
76
|
-
normalized: value,
|
|
77
|
-
fieldErrors: mapped.fieldErrors,
|
|
78
|
-
globalErrors: mapped.globalErrors,
|
|
79
|
-
issues: fallbackIssues
|
|
80
|
-
};
|
|
54
|
+
return buildValidationSuccessResult(normalized);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!isFieldValidationError(error)) {
|
|
57
|
+
throw error;
|
|
81
58
|
}
|
|
82
59
|
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
ok: false,
|
|
86
|
-
value: null,
|
|
87
|
-
normalized: value,
|
|
88
|
-
fieldErrors: {},
|
|
89
|
-
globalErrors: [fallbackMessage],
|
|
90
|
-
issues: []
|
|
91
|
-
};
|
|
60
|
+
return buildValidationFailureResult(error, value);
|
|
92
61
|
}
|
|
93
|
-
|
|
94
|
-
const issues = Check(schema, normalized) ? [] : [...Errors(schema, normalized)];
|
|
95
|
-
const mapped = mapOperationIssues(issues, schema);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
ok: issues.length < 1,
|
|
99
|
-
value: issues.length < 1 ? normalized : null,
|
|
100
|
-
normalized,
|
|
101
|
-
fieldErrors: mapped.fieldErrors,
|
|
102
|
-
globalErrors: mapped.globalErrors,
|
|
103
|
-
issues
|
|
104
|
-
};
|
|
105
62
|
}
|
|
106
63
|
|
|
107
64
|
function validateOperationInput({
|
|
@@ -109,55 +66,59 @@ function validateOperationInput({
|
|
|
109
66
|
input = {},
|
|
110
67
|
context = {}
|
|
111
68
|
} = {}) {
|
|
112
|
-
const source =
|
|
69
|
+
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
113
70
|
const sectionResults = {
|
|
114
|
-
|
|
71
|
+
params: validateOperationSection({
|
|
115
72
|
operation,
|
|
116
|
-
section: "
|
|
73
|
+
section: "params",
|
|
117
74
|
value: source.params,
|
|
118
75
|
context
|
|
119
76
|
}),
|
|
120
|
-
|
|
77
|
+
query: validateOperationSection({
|
|
121
78
|
operation,
|
|
122
|
-
section: "
|
|
79
|
+
section: "query",
|
|
123
80
|
value: source.query,
|
|
124
81
|
context
|
|
125
82
|
}),
|
|
126
|
-
|
|
83
|
+
body: validateOperationSection({
|
|
127
84
|
operation,
|
|
128
|
-
section: "
|
|
85
|
+
section: "body",
|
|
129
86
|
value: source.body,
|
|
130
87
|
context
|
|
131
88
|
})
|
|
132
89
|
};
|
|
133
90
|
|
|
134
91
|
const fieldErrors = {
|
|
135
|
-
...sectionResults.
|
|
136
|
-
...sectionResults.
|
|
137
|
-
...sectionResults.
|
|
92
|
+
...sectionResults.params.fieldErrors,
|
|
93
|
+
...sectionResults.query.fieldErrors,
|
|
94
|
+
...sectionResults.body.fieldErrors
|
|
138
95
|
};
|
|
139
96
|
|
|
140
97
|
const globalErrors = [
|
|
141
|
-
...sectionResults.
|
|
142
|
-
...sectionResults.
|
|
143
|
-
...sectionResults.
|
|
98
|
+
...sectionResults.params.globalErrors,
|
|
99
|
+
...sectionResults.query.globalErrors,
|
|
100
|
+
...sectionResults.body.globalErrors
|
|
144
101
|
];
|
|
145
102
|
|
|
146
103
|
return {
|
|
147
|
-
ok: sectionResults.
|
|
104
|
+
ok: sectionResults.params.ok && sectionResults.query.ok && sectionResults.body.ok,
|
|
148
105
|
value: {
|
|
149
|
-
params: sectionResults.
|
|
150
|
-
query: sectionResults.
|
|
151
|
-
body: sectionResults.
|
|
106
|
+
params: sectionResults.params.value,
|
|
107
|
+
query: sectionResults.query.value,
|
|
108
|
+
body: sectionResults.body.value
|
|
152
109
|
},
|
|
153
110
|
normalized: {
|
|
154
|
-
params: sectionResults.
|
|
155
|
-
query: sectionResults.
|
|
156
|
-
body: sectionResults.
|
|
111
|
+
params: sectionResults.params.normalized,
|
|
112
|
+
query: sectionResults.query.normalized,
|
|
113
|
+
body: sectionResults.body.normalized
|
|
157
114
|
},
|
|
158
115
|
fieldErrors,
|
|
159
116
|
globalErrors,
|
|
160
|
-
|
|
117
|
+
issues: [
|
|
118
|
+
...sectionResults.params.issues,
|
|
119
|
+
...sectionResults.query.issues,
|
|
120
|
+
...sectionResults.body.issues
|
|
121
|
+
]
|
|
161
122
|
};
|
|
162
123
|
}
|
|
163
124
|
|