@jskit-ai/http-runtime 0.1.54 → 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.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
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
"packageVersion": 1,
|
|
3
3
|
"packageId": "@jskit-ai/http-runtime",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.56",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"dependsOn": [],
|
|
7
7
|
"capabilities": {
|
|
@@ -67,9 +67,7 @@ export default Object.freeze({
|
|
|
67
67
|
"mutations": {
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"runtime": {
|
|
70
|
-
"@jskit-ai/kernel": "0.1.
|
|
71
|
-
"@fastify/type-provider-typebox": "^6.1.0",
|
|
72
|
-
"typebox": "^1.0.81"
|
|
70
|
+
"@jskit-ai/kernel": "0.1.57"
|
|
73
71
|
},
|
|
74
72
|
"dev": {}
|
|
75
73
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/http-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
10
|
"./shared": "./src/shared/index.js",
|
|
11
11
|
"./shared/validators/errorResponses": "./src/shared/validators/errorResponses.js",
|
|
12
|
+
"./shared/validators/jsonApiResult": "./src/shared/validators/jsonApiResult.js",
|
|
13
|
+
"./shared/validators/jsonApiRouteTransport": "./src/shared/validators/jsonApiRouteTransport.js",
|
|
12
14
|
"./shared/validators/paginationQuery": "./src/shared/validators/paginationQuery.js",
|
|
13
15
|
"./shared/validators/command": "./src/shared/validators/command.js",
|
|
14
16
|
"./shared/validators/resource": "./src/shared/validators/resource.js",
|
|
15
|
-
"./shared/validators/typeboxFormats": "./src/shared/validators/typeboxFormats.js",
|
|
16
17
|
"./shared/validators/operationMessages": "./src/shared/validators/operationMessages.js",
|
|
17
18
|
"./shared/validators/operationValidation": "./src/shared/validators/operationValidation.js"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@jskit-ai/kernel": "0.1.
|
|
21
|
-
"
|
|
22
|
-
"typebox": "^1.0.81"
|
|
21
|
+
"@jskit-ai/kernel": "0.1.57",
|
|
22
|
+
"json-rest-schema": "1.x.x"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { createHttpError, createNetworkError } from "./errors.js";
|
|
2
2
|
import { hasHeader, setHeaderIfMissing } from "./headers.js";
|
|
3
3
|
import { DEFAULT_RETRYABLE_CSRF_ERROR_CODES, shouldRetryForCsrfFailure } from "./retry.js";
|
|
4
|
+
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
5
|
+
import { isJsonContentType } from "../validators/jsonApiTransport.js";
|
|
6
|
+
import {
|
|
7
|
+
JSON_API_CONTENT_TYPE,
|
|
8
|
+
decodeJsonApiResourceResponse,
|
|
9
|
+
encodeJsonApiResourceRequestBody,
|
|
10
|
+
encodeJsonApiResourceQuery,
|
|
11
|
+
normalizeJsonApiClientTransport
|
|
12
|
+
} from "./jsonApiResourceTransport.js";
|
|
4
13
|
|
|
5
14
|
const DEFAULT_UNSAFE_METHODS = Object.freeze(["POST", "PUT", "PATCH", "DELETE"]);
|
|
6
15
|
const DEFAULT_NDJSON_CONTENT_TYPE = "application/x-ndjson";
|
|
@@ -28,9 +37,58 @@ function isObjectBody(value) {
|
|
|
28
37
|
return Boolean(value) && typeof value === "object" && !(value instanceof FormData);
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
function isQueryValuePresent(value) {
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.some((entry) => String(entry ?? "").trim());
|
|
43
|
+
}
|
|
44
|
+
return String(value ?? "").trim().length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appendRequestQueryToUrl(url, query = null, transport = null) {
|
|
48
|
+
const normalizedUrl = String(url || "").trim();
|
|
49
|
+
if (!normalizedUrl) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const encodedQuery =
|
|
54
|
+
transport
|
|
55
|
+
? encodeJsonApiResourceQuery(query, transport)
|
|
56
|
+
: query && typeof query === "object" && !Array.isArray(query)
|
|
57
|
+
? query
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
if (!encodedQuery || typeof encodedQuery !== "object" || Array.isArray(encodedQuery)) {
|
|
61
|
+
return normalizedUrl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const searchParams = new URLSearchParams();
|
|
65
|
+
for (const [key, rawValue] of Object.entries(encodedQuery)) {
|
|
66
|
+
const normalizedKey = String(key || "").trim();
|
|
67
|
+
if (!normalizedKey || !isQueryValuePresent(rawValue)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
72
|
+
for (const value of values) {
|
|
73
|
+
const normalizedValue = String(value ?? "").trim();
|
|
74
|
+
if (!normalizedValue) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
searchParams.append(normalizedKey, normalizedValue);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const serializedQuery = searchParams.toString();
|
|
82
|
+
if (!serializedQuery) {
|
|
83
|
+
return normalizedUrl;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return appendQueryString(normalizedUrl, serializedQuery);
|
|
87
|
+
}
|
|
88
|
+
|
|
31
89
|
function parseJsonSafely(response) {
|
|
32
90
|
const contentType = String(response?.headers?.get?.("content-type") || "");
|
|
33
|
-
const isJson = contentType
|
|
91
|
+
const isJson = isJsonContentType(contentType);
|
|
34
92
|
if (!isJson) {
|
|
35
93
|
return Promise.resolve({
|
|
36
94
|
contentType,
|
|
@@ -256,11 +314,18 @@ function createHttpClient(options = {}) {
|
|
|
256
314
|
const resolvedState = resolveRequestState(state);
|
|
257
315
|
|
|
258
316
|
const method = normalizeMethod(requestOptions.method);
|
|
317
|
+
const transport = normalizeJsonApiClientTransport(requestOptions.transport);
|
|
318
|
+
const {
|
|
319
|
+
transport: _transport,
|
|
320
|
+
query: requestQuery,
|
|
321
|
+
...forwardedRequestOptions
|
|
322
|
+
} = requestOptions && typeof requestOptions === "object" ? requestOptions : {};
|
|
323
|
+
const requestUrl = appendRequestQueryToUrl(url, requestQuery, transport);
|
|
259
324
|
const headers =
|
|
260
325
|
requestOptions.headers && typeof requestOptions.headers === "object" ? { ...requestOptions.headers } : {};
|
|
261
326
|
|
|
262
327
|
const decorateHeadersResult = decorateHeaders({
|
|
263
|
-
url,
|
|
328
|
+
url: requestUrl,
|
|
264
329
|
method,
|
|
265
330
|
headers,
|
|
266
331
|
requestOptions,
|
|
@@ -273,11 +338,20 @@ function createHttpClient(options = {}) {
|
|
|
273
338
|
|
|
274
339
|
const config = {
|
|
275
340
|
credentials: String(options?.credentials || "same-origin"),
|
|
276
|
-
...
|
|
341
|
+
...forwardedRequestOptions,
|
|
277
342
|
method,
|
|
278
343
|
headers
|
|
279
344
|
};
|
|
280
345
|
|
|
346
|
+
if (transport) {
|
|
347
|
+
setHeaderIfMissing(headers, "Accept", JSON_API_CONTENT_TYPE);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (transport && isObjectBody(config.body)) {
|
|
351
|
+
setHeaderIfMissing(headers, "Content-Type", JSON_API_CONTENT_TYPE);
|
|
352
|
+
config.body = encodeJsonApiResourceRequestBody(config.body, transport);
|
|
353
|
+
}
|
|
354
|
+
|
|
281
355
|
if (isObjectBody(config.body)) {
|
|
282
356
|
setHeaderIfMissing(headers, "Content-Type", "application/json");
|
|
283
357
|
config.body = JSON.stringify(config.body);
|
|
@@ -293,7 +367,9 @@ function createHttpClient(options = {}) {
|
|
|
293
367
|
return {
|
|
294
368
|
method,
|
|
295
369
|
config,
|
|
296
|
-
state: resolvedState
|
|
370
|
+
state: resolvedState,
|
|
371
|
+
transport,
|
|
372
|
+
url: requestUrl
|
|
297
373
|
};
|
|
298
374
|
}
|
|
299
375
|
|
|
@@ -383,7 +459,7 @@ function createHttpClient(options = {}) {
|
|
|
383
459
|
state: resolvedState
|
|
384
460
|
} = requestContext;
|
|
385
461
|
const result = await executePreparedRequest(
|
|
386
|
-
url,
|
|
462
|
+
requestContext.url,
|
|
387
463
|
requestContext.config,
|
|
388
464
|
requestContext,
|
|
389
465
|
(cause) =>
|
|
@@ -420,7 +496,8 @@ function createHttpClient(options = {}) {
|
|
|
420
496
|
value: {
|
|
421
497
|
method,
|
|
422
498
|
state: resolvedState,
|
|
423
|
-
result
|
|
499
|
+
result,
|
|
500
|
+
transport: requestContext.transport
|
|
424
501
|
}
|
|
425
502
|
};
|
|
426
503
|
}
|
|
@@ -454,20 +531,60 @@ function createHttpClient(options = {}) {
|
|
|
454
531
|
const {
|
|
455
532
|
method,
|
|
456
533
|
state: resolvedState,
|
|
457
|
-
result
|
|
534
|
+
result,
|
|
535
|
+
transport
|
|
458
536
|
} = execution.value;
|
|
459
537
|
|
|
538
|
+
if (Number(result.response?.status || 200) === 204) {
|
|
539
|
+
await notifySuccess({
|
|
540
|
+
url,
|
|
541
|
+
method,
|
|
542
|
+
state: resolvedState,
|
|
543
|
+
response: result.response,
|
|
544
|
+
data: null,
|
|
545
|
+
rawData: null,
|
|
546
|
+
contentType: result.contentType,
|
|
547
|
+
isJson: result.isJson,
|
|
548
|
+
stream: false
|
|
549
|
+
});
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let responseData = result.data;
|
|
554
|
+
if (transport) {
|
|
555
|
+
try {
|
|
556
|
+
responseData = decodeJsonApiResourceResponse(result.data, transport);
|
|
557
|
+
} catch (cause) {
|
|
558
|
+
const error = createNetworkError(cause);
|
|
559
|
+
error.message = "JSON:API response decoding failed.";
|
|
560
|
+
await notifyFailure({
|
|
561
|
+
url,
|
|
562
|
+
method,
|
|
563
|
+
state: resolvedState,
|
|
564
|
+
reason: "transport_decode_error",
|
|
565
|
+
error,
|
|
566
|
+
response: result.response,
|
|
567
|
+
data: result.data,
|
|
568
|
+
contentType: result.contentType,
|
|
569
|
+
isJson: result.isJson,
|
|
570
|
+
stream: false
|
|
571
|
+
});
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
460
576
|
await notifySuccess({
|
|
461
577
|
url,
|
|
462
578
|
method,
|
|
463
579
|
state: resolvedState,
|
|
464
580
|
response: result.response,
|
|
465
|
-
data:
|
|
581
|
+
data: responseData,
|
|
582
|
+
rawData: result.data,
|
|
466
583
|
contentType: result.contentType,
|
|
467
584
|
isJson: result.isJson,
|
|
468
585
|
stream: false
|
|
469
586
|
});
|
|
470
|
-
return
|
|
587
|
+
return responseData;
|
|
471
588
|
}
|
|
472
589
|
|
|
473
590
|
async function requestStream(url, requestOptions = {}, handlers = {}, state = null) {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { isRecord, resolveFieldErrors } from "../support/fieldErrors.js";
|
|
2
|
+
import { createJsonApiClientErrorPayload } from "./jsonApiResourceTransport.js";
|
|
2
3
|
|
|
3
4
|
function createHttpError(response, data = {}) {
|
|
4
5
|
const payload = isRecord(data) ? data : {};
|
|
6
|
+
const jsonApiPayload = createJsonApiClientErrorPayload(payload);
|
|
7
|
+
if (jsonApiPayload) {
|
|
8
|
+
return createHttpError(response, jsonApiPayload);
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
const error = new Error(payload.error || `Request failed with status ${response.status}.`);
|
|
6
12
|
const normalizedFieldErrors = resolveFieldErrors(payload);
|
|
7
13
|
error.status = Number(response?.status || 0);
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { normalizeArray, normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import {
|
|
3
|
+
JSON_API_CONTENT_TYPE,
|
|
4
|
+
createJsonApiDocument,
|
|
5
|
+
createJsonApiResourceObject,
|
|
6
|
+
normalizeJsonApiDocument,
|
|
7
|
+
resolveJsonApiTransportTypes
|
|
8
|
+
} from "../validators/jsonApiTransport.js";
|
|
9
|
+
import { encodeJsonApiResourceQueryObject } from "../validators/jsonApiQueryTransport.js";
|
|
10
|
+
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeTransportKind(transport = null) {
|
|
16
|
+
return String(transport?.kind || "").trim().toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isJsonApiResourceTransport(transport = null) {
|
|
20
|
+
return normalizeTransportKind(transport) === "jsonapi-resource";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeJsonApiClientTransport(transport = null) {
|
|
24
|
+
if (!isJsonApiResourceTransport(transport)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resolvedTypes = resolveJsonApiTransportTypes(transport, {
|
|
29
|
+
context: "JSON:API client transport"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
kind: "jsonapi-resource",
|
|
34
|
+
requestType: resolvedTypes.requestType,
|
|
35
|
+
responseType: resolvedTypes.responseType,
|
|
36
|
+
responseKind: normalizeText(transport?.responseKind, {
|
|
37
|
+
fallback: "record"
|
|
38
|
+
}).toLowerCase(),
|
|
39
|
+
includeBodyId: transport?.includeBodyId === true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultEncodeAttributes(body = {}) {
|
|
44
|
+
const source = normalizeObject(body);
|
|
45
|
+
const attributes = {
|
|
46
|
+
...source
|
|
47
|
+
};
|
|
48
|
+
delete attributes.id;
|
|
49
|
+
return attributes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function encodeJsonApiResourceRequestBody(body, transport = null) {
|
|
53
|
+
const normalizedTransport = normalizeJsonApiClientTransport(transport);
|
|
54
|
+
if (!normalizedTransport) {
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!isRecord(body)) {
|
|
59
|
+
throw new TypeError("JSON:API resource request body must be an object.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!normalizedTransport.requestType) {
|
|
63
|
+
throw new TypeError("JSON:API resource request body requires requestType.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const source = normalizeObject(body);
|
|
67
|
+
return createJsonApiDocument({
|
|
68
|
+
data: createJsonApiResourceObject({
|
|
69
|
+
type: normalizedTransport.requestType,
|
|
70
|
+
...(normalizedTransport.includeBodyId && source.id != null ? { id: source.id } : {}),
|
|
71
|
+
attributes: defaultEncodeAttributes(source)
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function encodeJsonApiResourceQuery(query, transport = null) {
|
|
77
|
+
const normalizedTransport = normalizeJsonApiClientTransport(transport);
|
|
78
|
+
if (!normalizedTransport) {
|
|
79
|
+
return query;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return encodeJsonApiResourceQueryObject(query, {
|
|
83
|
+
responseType: normalizedTransport.responseType
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function simplifyResourceObject(resource = {}) {
|
|
88
|
+
const normalizedResource = isRecord(resource) ? normalizeObject(resource) : {};
|
|
89
|
+
return {
|
|
90
|
+
id: normalizedResource.id == null ? "" : String(normalizedResource.id),
|
|
91
|
+
...(normalizeObject(normalizedResource.attributes))
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertPrimaryDataType(resource = {}, expectedType = "") {
|
|
96
|
+
const normalizedExpectedType = normalizeText(expectedType);
|
|
97
|
+
if (!normalizedExpectedType) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const actualType = normalizeText(resource?.type);
|
|
102
|
+
if (actualType && actualType !== normalizedExpectedType) {
|
|
103
|
+
throw new Error(`Expected JSON:API resource type ${normalizedExpectedType}, received ${actualType}.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveCollectionPageMeta(document = {}) {
|
|
108
|
+
const nextCursor = normalizeText(
|
|
109
|
+
document?.meta?.page?.nextCursor || document?.meta?.pagination?.cursor?.next
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
nextCursor: nextCursor || null,
|
|
113
|
+
...(isRecord(document?.meta) ? { meta: document.meta } : {}),
|
|
114
|
+
...(isRecord(document?.links) ? { links: document.links } : {})
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function decodeJsonApiResourceResponse(payload, transport = null) {
|
|
119
|
+
const normalizedTransport = normalizeJsonApiClientTransport(transport);
|
|
120
|
+
if (!normalizedTransport) {
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const document = normalizeJsonApiDocument(payload);
|
|
125
|
+
if (document.kind === "unknown") {
|
|
126
|
+
throw new Error("Expected JSON:API response document.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (normalizedTransport.responseKind === "collection") {
|
|
130
|
+
if (document.kind !== "collection") {
|
|
131
|
+
throw new Error("Expected JSON:API collection document.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const entry of document.data) {
|
|
135
|
+
assertPrimaryDataType(entry, normalizedTransport.responseType);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
items: document.data.map((entry) => simplifyResourceObject(entry)),
|
|
140
|
+
...resolveCollectionPageMeta(document)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (normalizedTransport.responseKind === "meta") {
|
|
145
|
+
if (document.kind !== "meta") {
|
|
146
|
+
throw new Error("Expected JSON:API meta document.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return isRecord(document.meta) ? document.meta : {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (normalizedTransport.responseKind === "nullable-record") {
|
|
153
|
+
if (document.kind !== "resource") {
|
|
154
|
+
throw new Error("Expected JSON:API resource document.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (document.data != null) {
|
|
158
|
+
assertPrimaryDataType(document.data, normalizedTransport.responseType);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return document.data == null ? null : simplifyResourceObject(document.data);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (document.kind !== "resource") {
|
|
165
|
+
throw new Error("Expected JSON:API resource document.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (document.data != null) {
|
|
169
|
+
assertPrimaryDataType(document.data, normalizedTransport.responseType);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return document.data == null ? null : simplifyResourceObject(document.data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function decodeJsonApiErrorFieldErrors(payload = {}) {
|
|
176
|
+
const document = normalizeJsonApiDocument(payload);
|
|
177
|
+
if (document.kind !== "errors") {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const fieldErrors = {};
|
|
182
|
+
for (const error of normalizeArray(document.errors)) {
|
|
183
|
+
const pointer = normalizeText(error?.source?.pointer);
|
|
184
|
+
const parameter = normalizeText(error?.source?.parameter);
|
|
185
|
+
const detail = normalizeText(error?.detail || error?.title, {
|
|
186
|
+
fallback: "Invalid value."
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (parameter && !Object.hasOwn(fieldErrors, parameter)) {
|
|
190
|
+
fieldErrors[parameter] = detail;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!pointer.startsWith("/data/attributes/")) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const fieldName = normalizeText(pointer.slice("/data/attributes/".length).split("/")[0]);
|
|
199
|
+
if (fieldName && !Object.hasOwn(fieldErrors, fieldName)) {
|
|
200
|
+
fieldErrors[fieldName] = detail;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return fieldErrors;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createJsonApiClientErrorPayload(payload = {}) {
|
|
208
|
+
const document = normalizeJsonApiDocument(payload);
|
|
209
|
+
if (document.kind !== "errors") {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const firstError = normalizeArray(document.errors)[0] || {};
|
|
214
|
+
const fieldErrors = decodeJsonApiErrorFieldErrors(payload);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
error: normalizeText(firstError.detail || firstError.title, {
|
|
218
|
+
fallback: "Request failed."
|
|
219
|
+
}),
|
|
220
|
+
code: normalizeText(firstError.code) || null,
|
|
221
|
+
...(Object.keys(fieldErrors).length > 0
|
|
222
|
+
? {
|
|
223
|
+
fieldErrors,
|
|
224
|
+
details: {
|
|
225
|
+
fieldErrors
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
: {})
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
JSON_API_CONTENT_TYPE,
|
|
234
|
+
isJsonApiResourceTransport,
|
|
235
|
+
normalizeJsonApiClientTransport,
|
|
236
|
+
encodeJsonApiResourceRequestBody,
|
|
237
|
+
encodeJsonApiResourceQuery,
|
|
238
|
+
decodeJsonApiResourceResponse,
|
|
239
|
+
decodeJsonApiErrorFieldErrors,
|
|
240
|
+
createJsonApiClientErrorPayload
|
|
241
|
+
};
|
package/src/shared/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export { createPaginationQuerySchema } from "./validators/paginationQuery.js";
|
|
2
|
-
export { registerTypeBoxFormats, __testables } from "./validators/typeboxFormats.js";
|
|
3
2
|
export {
|
|
4
|
-
|
|
3
|
+
fieldErrorsFieldDefinition,
|
|
5
4
|
apiErrorDetailsSchema,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
apiValidationErrorDetailsSchema,
|
|
6
|
+
apiErrorOutputValidator,
|
|
7
|
+
apiValidationErrorOutputValidator,
|
|
8
|
+
apiErrorTransportSchema,
|
|
9
|
+
apiValidationErrorTransportSchema,
|
|
10
|
+
fastifyDefaultErrorTransportSchema,
|
|
9
11
|
STANDARD_ERROR_STATUS_CODES,
|
|
12
|
+
createTransportResponseSchema,
|
|
10
13
|
passthroughErrorResponses,
|
|
11
14
|
withStandardErrorResponses,
|
|
12
15
|
enumSchema
|
|
@@ -28,3 +31,49 @@ export {
|
|
|
28
31
|
validateOperationSection,
|
|
29
32
|
validateOperationInput
|
|
30
33
|
} from "./validators/operationValidation.js";
|
|
34
|
+
export {
|
|
35
|
+
JSON_API_CONTENT_TYPE,
|
|
36
|
+
createJsonApiDocument,
|
|
37
|
+
createJsonApiErrorDocumentFromFailure,
|
|
38
|
+
createJsonApiErrorObject,
|
|
39
|
+
createJsonApiResourceObject,
|
|
40
|
+
isJsonApiCollectionDocument,
|
|
41
|
+
isJsonApiContentType,
|
|
42
|
+
isJsonApiErrorDocument,
|
|
43
|
+
isJsonApiResourceDocument,
|
|
44
|
+
isJsonContentType,
|
|
45
|
+
normalizeJsonApiDocument,
|
|
46
|
+
normalizeJsonApiResourceObject,
|
|
47
|
+
resolveJsonApiTransportTypes,
|
|
48
|
+
simplifyJsonApiDocument
|
|
49
|
+
} from "./validators/jsonApiTransport.js";
|
|
50
|
+
export {
|
|
51
|
+
returnJsonApiData,
|
|
52
|
+
returnJsonApiDocument,
|
|
53
|
+
returnJsonApiMeta,
|
|
54
|
+
isJsonApiResult,
|
|
55
|
+
isJsonApiDataResult,
|
|
56
|
+
isJsonApiDocumentResult,
|
|
57
|
+
isJsonApiMetaResult,
|
|
58
|
+
unwrapJsonApiResult
|
|
59
|
+
} from "./validators/jsonApiResult.js";
|
|
60
|
+
export {
|
|
61
|
+
JSON_API_QUERY_PAGE_CURSOR_KEY,
|
|
62
|
+
JSON_API_QUERY_PAGE_LIMIT_KEY,
|
|
63
|
+
JSON_API_QUERY_INCLUDE_KEY,
|
|
64
|
+
JSON_API_QUERY_SORT_KEY,
|
|
65
|
+
mapPlainQueryKeyToTransportKey,
|
|
66
|
+
mapTransportQueryKeyToPlainKey,
|
|
67
|
+
encodeJsonApiResourceQueryObject,
|
|
68
|
+
decodeJsonApiResourceQueryObject,
|
|
69
|
+
createJsonApiResourceQueryTransportSchema
|
|
70
|
+
} from "./validators/jsonApiQueryTransport.js";
|
|
71
|
+
export {
|
|
72
|
+
JSON_API_ERROR_DOCUMENT_SCHEMA,
|
|
73
|
+
createJsonApiResourceObjectTransportSchema,
|
|
74
|
+
createJsonApiResourceRequestBodyTransportSchema,
|
|
75
|
+
createJsonApiResourceSuccessTransportSchema,
|
|
76
|
+
withJsonApiErrorResponses,
|
|
77
|
+
createJsonApiResourceRouteTransport,
|
|
78
|
+
createJsonApiResourceRouteContract
|
|
79
|
+
} from "./validators/jsonApiRouteTransport.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { asSchemaDefinition } from "./schemaUtils.js";
|
|
2
2
|
import { normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
3
4
|
|
|
4
5
|
function createCommand({
|
|
5
6
|
input,
|
|
@@ -8,8 +9,8 @@ function createCommand({
|
|
|
8
9
|
invalidates = []
|
|
9
10
|
} = {}) {
|
|
10
11
|
const command = {
|
|
11
|
-
input:
|
|
12
|
-
output:
|
|
12
|
+
input: asSchemaDefinition(input, "input", "patch"),
|
|
13
|
+
output: asSchemaDefinition(output, "output", "replace"),
|
|
13
14
|
invalidates: Object.freeze(normalizeUniqueTextList(invalidates))
|
|
14
15
|
};
|
|
15
16
|
|
|
@@ -17,7 +18,7 @@ function createCommand({
|
|
|
17
18
|
command.idempotent = idempotent;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
return
|
|
21
|
+
return deepFreeze(command);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export { createCommand };
|