@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.
Files changed (32) hide show
  1. package/package.descriptor.mjs +2 -4
  2. package/package.json +5 -5
  3. package/src/shared/clientRuntime/client.js +126 -9
  4. package/src/shared/clientRuntime/errors.js +6 -0
  5. package/src/shared/clientRuntime/jsonApiResourceTransport.js +241 -0
  6. package/src/shared/index.js +54 -5
  7. package/src/shared/validators/command.js +5 -4
  8. package/src/shared/validators/errorResponses.js +125 -62
  9. package/src/shared/validators/httpValidatorsApi.js +83 -12
  10. package/src/shared/validators/jsonApiQueryTransport.js +211 -0
  11. package/src/shared/validators/jsonApiResponses.js +3 -0
  12. package/src/shared/validators/jsonApiResult.js +83 -0
  13. package/src/shared/validators/jsonApiRouteTransport.js +800 -0
  14. package/src/shared/validators/jsonApiTransport.js +484 -0
  15. package/src/shared/validators/operationValidation.js +62 -101
  16. package/src/shared/validators/paginationQuery.js +14 -19
  17. package/src/shared/validators/resource.js +15 -17
  18. package/src/shared/validators/schemaUtils.js +18 -5
  19. package/src/shared/validators/transportSchemaEmbedding.js +81 -0
  20. package/test/client.test.js +279 -0
  21. package/test/command.test.js +38 -21
  22. package/test/entrypoints.boundary.test.js +8 -0
  23. package/test/errorResponses.test.js +49 -13
  24. package/test/jsonApiRouteTransport.test.js +349 -0
  25. package/test/jsonApiTransport.test.js +231 -0
  26. package/test/operationMessages.test.js +115 -66
  27. package/test/operationValidation.test.js +147 -159
  28. package/test/paginationQuery.test.js +4 -8
  29. package/test/resource.test.js +89 -55
  30. package/test/validationErrors.test.js +33 -0
  31. package/src/shared/validators/typeboxFormats.js +0 -43
  32. package/test/typeboxFormats.test.js +0 -42
@@ -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.54",
4
+ "version": "0.1.55",
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.55",
71
- "@fastify/type-provider-typebox": "^6.1.0",
72
- "typebox": "^1.0.81"
70
+ "@jskit-ai/kernel": "0.1.56"
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.54",
3
+ "version": "0.1.55",
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.55",
21
- "@fastify/type-provider-typebox": "^6.1.0",
22
- "typebox": "^1.0.81"
21
+ "@jskit-ai/kernel": "0.1.56",
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.includes("application/json");
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
- ...requestOptions,
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: result.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 result.data;
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
+ };
@@ -1,12 +1,15 @@
1
1
  export { createPaginationQuerySchema } from "./validators/paginationQuery.js";
2
- export { registerTypeBoxFormats, __testables } from "./validators/typeboxFormats.js";
3
2
  export {
4
- fieldErrorsSchema,
3
+ fieldErrorsFieldDefinition,
5
4
  apiErrorDetailsSchema,
6
- apiErrorResponseSchema,
7
- apiValidationErrorResponseSchema,
8
- fastifyDefaultErrorResponseSchema,
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 { asSchema } from "./schemaUtils.js";
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: asSchema(input, "input"),
12
- output: asSchema(output, "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 Object.freeze(command);
21
+ return deepFreeze(command);
21
22
  }
22
23
 
23
24
  export { createCommand };