@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.
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
@@ -0,0 +1,800 @@
1
+ import { normalizeArray } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ resolveSchemaTransportSchemaDefinition
5
+ } from "@jskit-ai/kernel/shared/validators";
6
+ import {
7
+ JSON_API_CONTENT_TYPE,
8
+ createJsonApiDocument,
9
+ createJsonApiErrorDocumentFromFailure,
10
+ createJsonApiResourceObject,
11
+ normalizeJsonApiDocument,
12
+ resolveJsonApiTransportTypes
13
+ } from "./jsonApiTransport.js";
14
+ import {
15
+ isJsonApiDataResult,
16
+ isJsonApiDocumentResult,
17
+ isJsonApiMetaResult,
18
+ unwrapJsonApiResult
19
+ } from "./jsonApiResult.js";
20
+ import {
21
+ createJsonApiResourceQueryTransportSchema,
22
+ decodeJsonApiResourceQueryObject
23
+ } from "./jsonApiQueryTransport.js";
24
+ import {
25
+ STANDARD_ERROR_STATUS_CODES,
26
+ createTransportResponseSchema
27
+ } from "./errorResponses.js";
28
+ import { createEmbeddableTransportSchemaDocument } from "./transportSchemaEmbedding.js";
29
+
30
+ const JSON_API_ID_SCHEMA = Object.freeze({
31
+ anyOf: [
32
+ { type: "string", minLength: 1 },
33
+ { type: "number" }
34
+ ]
35
+ });
36
+
37
+ const JSON_API_LINK_VALUE_SCHEMA = Object.freeze({
38
+ anyOf: [
39
+ { type: "string", minLength: 1 },
40
+ {
41
+ type: "object",
42
+ additionalProperties: true
43
+ }
44
+ ]
45
+ });
46
+
47
+ const JSON_API_LINKS_SCHEMA = Object.freeze({
48
+ type: "object",
49
+ additionalProperties: JSON_API_LINK_VALUE_SCHEMA
50
+ });
51
+
52
+ const JSON_API_META_SCHEMA = Object.freeze({
53
+ type: "object",
54
+ additionalProperties: true
55
+ });
56
+
57
+ const JSON_API_ERROR_OBJECT_SCHEMA = Object.freeze({
58
+ type: "object",
59
+ additionalProperties: false,
60
+ properties: {
61
+ status: { type: "string", minLength: 1 },
62
+ code: { type: "string", minLength: 1 },
63
+ title: { type: "string", minLength: 1 },
64
+ detail: { type: "string", minLength: 1 },
65
+ source: {
66
+ type: "object",
67
+ additionalProperties: true
68
+ },
69
+ links: JSON_API_LINKS_SCHEMA,
70
+ meta: JSON_API_META_SCHEMA
71
+ }
72
+ });
73
+
74
+ const JSON_API_ERROR_DOCUMENT_SCHEMA = Object.freeze({
75
+ type: "object",
76
+ additionalProperties: false,
77
+ required: ["errors"],
78
+ properties: {
79
+ errors: {
80
+ type: "array",
81
+ minItems: 1,
82
+ items: JSON_API_ERROR_OBJECT_SCHEMA
83
+ },
84
+ links: JSON_API_LINKS_SCHEMA,
85
+ meta: JSON_API_META_SCHEMA
86
+ }
87
+ });
88
+
89
+ function isRecord(value) {
90
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
91
+ }
92
+
93
+ function createJsonApiTransportError(statusCode, message, code) {
94
+ const error = new Error(String(message || "JSON:API transport error."));
95
+ error.status = Number(statusCode) || 500;
96
+ error.statusCode = error.status;
97
+ error.code = String(code || "jsonapi_transport_error").trim() || "jsonapi_transport_error";
98
+ return error;
99
+ }
100
+
101
+ function resolveRouteType(type = "") {
102
+ const normalizedType = String(type || "").trim();
103
+ if (!normalizedType) {
104
+ throw new TypeError("JSON:API resource transport requires a non-empty type.");
105
+ }
106
+ return normalizedType;
107
+ }
108
+
109
+ function resolveRouteTypes(value = {}) {
110
+ const resolvedTypes = resolveJsonApiTransportTypes(value, {
111
+ context: "JSON:API resource transport"
112
+ });
113
+
114
+ return Object.freeze({
115
+ requestType: resolvedTypes.requestType ? resolveRouteType(resolvedTypes.requestType) : "",
116
+ responseType: resolvedTypes.responseType ? resolveRouteType(resolvedTypes.responseType) : ""
117
+ });
118
+ }
119
+
120
+ function resolveEmbeddedAttributesTransportSchema(definition, {
121
+ context = "JSON:API resource",
122
+ defaultMode = "replace",
123
+ removeId = false
124
+ } = {}) {
125
+ const transportSchema = resolveSchemaTransportSchemaDefinition(definition, {
126
+ context,
127
+ defaultMode
128
+ });
129
+
130
+ if (!transportSchema || typeof transportSchema !== "object" || Array.isArray(transportSchema)) {
131
+ throw new TypeError(`${context} transport schema must resolve to an object schema.`);
132
+ }
133
+
134
+ const sourceSchema = normalizeObject(transportSchema);
135
+ const properties = normalizeObject(sourceSchema.properties);
136
+ const nextProperties = {
137
+ ...properties
138
+ };
139
+
140
+ if (removeId) {
141
+ delete nextProperties.id;
142
+ }
143
+
144
+ const required = Array.isArray(sourceSchema.required)
145
+ ? sourceSchema.required.filter((entry) => String(entry || "").trim() && (!removeId || entry !== "id"))
146
+ : [];
147
+
148
+ const attributeSchema = {
149
+ ...sourceSchema,
150
+ type: "object",
151
+ properties: nextProperties
152
+ };
153
+
154
+ if (required.length > 0) {
155
+ attributeSchema.required = required;
156
+ } else {
157
+ delete attributeSchema.required;
158
+ }
159
+
160
+ return createEmbeddableTransportSchemaDocument(attributeSchema, `${normalizeText(context, { fallback: "JsonApiAttributes" }).replace(/[^a-z0-9]+/gi, "_")}`);
161
+ }
162
+
163
+ function createJsonApiResourceObjectTransportSchema({
164
+ type = "",
165
+ attributes,
166
+ requireId = true,
167
+ includeLinks = false,
168
+ includeMeta = false
169
+ } = {}) {
170
+ const normalizedType = resolveRouteType(type);
171
+ const embeddedAttributes = resolveEmbeddedAttributesTransportSchema(attributes, {
172
+ context: `${normalizedType} resource attributes`,
173
+ defaultMode: "replace",
174
+ removeId: true
175
+ });
176
+
177
+ const properties = {
178
+ type: {
179
+ const: normalizedType
180
+ },
181
+ attributes: embeddedAttributes.schema
182
+ };
183
+ const required = ["type", "attributes"];
184
+
185
+ if (requireId) {
186
+ properties.id = JSON_API_ID_SCHEMA;
187
+ required.push("id");
188
+ }
189
+
190
+ if (includeLinks) {
191
+ properties.links = JSON_API_LINKS_SCHEMA;
192
+ }
193
+ if (includeMeta) {
194
+ properties.meta = JSON_API_META_SCHEMA;
195
+ }
196
+
197
+ return {
198
+ schema: {
199
+ type: "object",
200
+ additionalProperties: false,
201
+ required,
202
+ properties
203
+ },
204
+ definitions: embeddedAttributes.definitions
205
+ };
206
+ }
207
+
208
+ function createJsonApiResourceRequestBodyTransportSchema({
209
+ type = "",
210
+ attributes,
211
+ requireId = false
212
+ } = {}) {
213
+ const resourceTransport = createJsonApiResourceObjectTransportSchema({
214
+ type,
215
+ attributes,
216
+ requireId
217
+ });
218
+ const embeddedResource = createEmbeddableTransportSchemaDocument(
219
+ {
220
+ ...resourceTransport.schema,
221
+ definitions: resourceTransport.definitions
222
+ },
223
+ `${resolveRouteType(type)}RequestResource`
224
+ );
225
+
226
+ return {
227
+ type: "object",
228
+ additionalProperties: false,
229
+ required: ["data"],
230
+ properties: {
231
+ data: embeddedResource.schema
232
+ },
233
+ definitions: embeddedResource.definitions
234
+ };
235
+ }
236
+
237
+ function createJsonApiMetaSuccessTransportSchema({
238
+ meta
239
+ } = {}) {
240
+ const embeddedMeta = resolveEmbeddedAttributesTransportSchema(meta, {
241
+ context: "JSON:API success meta",
242
+ defaultMode: "replace",
243
+ removeId: false
244
+ });
245
+
246
+ return {
247
+ type: "object",
248
+ additionalProperties: false,
249
+ required: ["meta"],
250
+ properties: {
251
+ meta: embeddedMeta.schema
252
+ },
253
+ definitions: embeddedMeta.definitions
254
+ };
255
+ }
256
+
257
+ function createJsonApiResourceSuccessTransportSchema({
258
+ type = "",
259
+ attributes,
260
+ kind = "record",
261
+ includeLinks = false,
262
+ includeMeta = false,
263
+ includeIncluded = false
264
+ } = {}) {
265
+ const normalizedKind = String(kind || "record").trim().toLowerCase();
266
+ if (!["record", "nullable-record", "collection", "meta"].includes(normalizedKind)) {
267
+ throw new TypeError(`Unsupported JSON:API success schema kind: ${normalizedKind || "<empty>"}.`);
268
+ }
269
+
270
+ if (normalizedKind === "meta") {
271
+ return createJsonApiMetaSuccessTransportSchema({
272
+ meta: attributes
273
+ });
274
+ }
275
+
276
+ const resourceTransport = createJsonApiResourceObjectTransportSchema({
277
+ type,
278
+ attributes,
279
+ requireId: true
280
+ });
281
+ const embeddedResource = createEmbeddableTransportSchemaDocument(
282
+ {
283
+ ...resourceTransport.schema,
284
+ definitions: resourceTransport.definitions
285
+ },
286
+ `${resolveRouteType(type)}SuccessResource`
287
+ );
288
+
289
+ const documentProperties = {};
290
+ const documentDefinitions = {
291
+ ...resourceTransport.definitions,
292
+ ...embeddedResource.definitions
293
+ };
294
+
295
+ if (normalizedKind === "collection") {
296
+ documentProperties.data = {
297
+ type: "array",
298
+ items: embeddedResource.schema
299
+ };
300
+ } else if (normalizedKind === "nullable-record") {
301
+ documentProperties.data = {
302
+ anyOf: [
303
+ { type: "null" },
304
+ embeddedResource.schema
305
+ ]
306
+ };
307
+ } else {
308
+ documentProperties.data = embeddedResource.schema;
309
+ }
310
+
311
+ if (includeIncluded) {
312
+ documentProperties.included = {
313
+ type: "array",
314
+ items: embeddedResource.schema
315
+ };
316
+ }
317
+ if (includeLinks) {
318
+ documentProperties.links = JSON_API_LINKS_SCHEMA;
319
+ }
320
+ if (includeMeta) {
321
+ documentProperties.meta = JSON_API_META_SCHEMA;
322
+ }
323
+
324
+ return {
325
+ type: "object",
326
+ additionalProperties: false,
327
+ required: ["data"],
328
+ properties: documentProperties,
329
+ definitions: documentDefinitions
330
+ };
331
+ }
332
+
333
+ function withJsonApiErrorResponses(successResponses, { includeValidation400 = false } = {}) {
334
+ const responses = {
335
+ ...normalizeObject(successResponses)
336
+ };
337
+
338
+ for (const statusCode of STANDARD_ERROR_STATUS_CODES) {
339
+ if (responses[statusCode]) {
340
+ continue;
341
+ }
342
+
343
+ if (statusCode === 400 && !includeValidation400) {
344
+ continue;
345
+ }
346
+
347
+ responses[statusCode] = createTransportResponseSchema(JSON_API_ERROR_DOCUMENT_SCHEMA);
348
+ }
349
+
350
+ return responses;
351
+ }
352
+
353
+ function defaultRecordTypeResolver(type) {
354
+ return function resolveRecordType() {
355
+ return type;
356
+ };
357
+ }
358
+
359
+ function defaultRecordIdResolver(record = {}) {
360
+ if (!isRecord(record) || record.id == null || String(record.id).trim() === "") {
361
+ throw createJsonApiTransportError(500, "JSON:API resource response requires record.id.", "jsonapi_record_id_missing");
362
+ }
363
+
364
+ return record.id;
365
+ }
366
+
367
+ function defaultRecordAttributesResolver(record = {}) {
368
+ if (!isRecord(record)) {
369
+ return {};
370
+ }
371
+
372
+ const attributes = {
373
+ ...record
374
+ };
375
+ delete attributes.id;
376
+ return attributes;
377
+ }
378
+
379
+ function defaultCollectionItemsResolver(payload) {
380
+ if (Array.isArray(payload)) {
381
+ return payload;
382
+ }
383
+
384
+ return Array.isArray(payload?.items) ? payload.items : [];
385
+ }
386
+
387
+ function defaultCollectionMetaResolver(payload) {
388
+ const nextCursor = String(payload?.nextCursor || "").trim();
389
+ if (!nextCursor) {
390
+ return undefined;
391
+ }
392
+
393
+ return {
394
+ page: {
395
+ nextCursor
396
+ }
397
+ };
398
+ }
399
+
400
+ function assertJsonApiSuccessResult(payload) {
401
+ const result = unwrapJsonApiResult(payload);
402
+ if (!result) {
403
+ throw createJsonApiTransportError(
404
+ 500,
405
+ "JSON:API route success payload must be returned with an explicit JSON:API result wrapper.",
406
+ "jsonapi_success_result_missing"
407
+ );
408
+ }
409
+
410
+ return result;
411
+ }
412
+
413
+ function createJsonApiResourceRouteTransport({
414
+ type = "",
415
+ requestType = "",
416
+ responseType = "",
417
+ query = null,
418
+ allowBodyId = false,
419
+ successKind = "record",
420
+ pointerPrefix = "/data/attributes",
421
+ mapRequestRelationships = null,
422
+ getRecordType = null,
423
+ getRecordId = null,
424
+ getRecordAttributes = null,
425
+ getRecordRelationships = null,
426
+ getRecordLinks = null,
427
+ getRecordMeta = null,
428
+ getIncluded = null,
429
+ getDocumentLinks = null,
430
+ getDocumentMeta = null,
431
+ getCollectionItems = null
432
+ } = {}) {
433
+ const resolvedTypes = resolveRouteTypes({
434
+ type,
435
+ requestType,
436
+ responseType
437
+ });
438
+ const normalizedRequestType = resolvedTypes.requestType;
439
+ const normalizedResponseType = resolvedTypes.responseType;
440
+ const normalizedSuccessKind = String(successKind || "record").trim().toLowerCase();
441
+
442
+ const resolveRecordType = typeof getRecordType === "function" ? getRecordType : defaultRecordTypeResolver(normalizedResponseType);
443
+ const resolveRecordId = typeof getRecordId === "function" ? getRecordId : defaultRecordIdResolver;
444
+ const resolveRecordAttributes = typeof getRecordAttributes === "function" ? getRecordAttributes : defaultRecordAttributesResolver;
445
+ const resolveCollectionItems = typeof getCollectionItems === "function" ? getCollectionItems : defaultCollectionItemsResolver;
446
+ const resolveDocumentMeta = typeof getDocumentMeta === "function" ? getDocumentMeta : defaultCollectionMetaResolver;
447
+
448
+ function buildResourceObject(record, context) {
449
+ if (!isRecord(record)) {
450
+ throw createJsonApiTransportError(500, "JSON:API resource response requires an object record.", "jsonapi_record_invalid");
451
+ }
452
+
453
+ const resourceOptions = {
454
+ type: resolveRecordType(record, context),
455
+ id: resolveRecordId(record, context),
456
+ attributes: resolveRecordAttributes(record, context)
457
+ };
458
+
459
+ if (typeof getRecordRelationships === "function") {
460
+ const relationships = getRecordRelationships(record, context);
461
+ if (relationships !== undefined) {
462
+ resourceOptions.relationships = relationships;
463
+ }
464
+ }
465
+ if (typeof getRecordLinks === "function") {
466
+ const links = getRecordLinks(record, context);
467
+ if (links !== undefined) {
468
+ resourceOptions.links = links;
469
+ }
470
+ }
471
+ if (typeof getRecordMeta === "function") {
472
+ const meta = getRecordMeta(record, context);
473
+ if (meta !== undefined) {
474
+ resourceOptions.meta = meta;
475
+ }
476
+ }
477
+
478
+ return createJsonApiResourceObject(resourceOptions);
479
+ }
480
+
481
+ return Object.freeze({
482
+ kind: "jsonapi-resource",
483
+ contentType: JSON_API_CONTENT_TYPE,
484
+ request: {
485
+ body(payload) {
486
+ if (!normalizedRequestType) {
487
+ throw createJsonApiTransportError(500, "JSON:API request transport requires requestType for body parsing.", "jsonapi_request_type_missing");
488
+ }
489
+
490
+ const document = normalizeJsonApiDocument(payload);
491
+ if (document.kind !== "resource" || document.data == null) {
492
+ throw createJsonApiTransportError(400, "JSON:API request body must contain a resource document.", "jsonapi_request_body_invalid");
493
+ }
494
+
495
+ if (String(document.data.type || "").trim() !== normalizedRequestType) {
496
+ throw createJsonApiTransportError(409, `JSON:API resource type must be ${normalizedRequestType}.`, "jsonapi_request_type_mismatch");
497
+ }
498
+
499
+ const nextBody = {
500
+ ...(document.data.attributes || {})
501
+ };
502
+
503
+ if (allowBodyId && document.data.id != null && String(document.data.id).trim()) {
504
+ nextBody.id = document.data.id;
505
+ }
506
+
507
+ if (typeof mapRequestRelationships === "function") {
508
+ Object.assign(nextBody, mapRequestRelationships(document.data.relationships || {}, {
509
+ payload,
510
+ document
511
+ }) || {});
512
+ }
513
+
514
+ return nextBody;
515
+ },
516
+ query(payload) {
517
+ if (!query) {
518
+ return payload;
519
+ }
520
+
521
+ return decodeJsonApiResourceQueryObject(payload, {
522
+ responseType: normalizedResponseType
523
+ });
524
+ }
525
+ },
526
+ response(payload, context = {}) {
527
+ if (normalizedSuccessKind === "no-content" || Number(context?.statusCode || 200) === 204) {
528
+ return undefined;
529
+ }
530
+
531
+ const result = assertJsonApiSuccessResult(payload);
532
+
533
+ if (isJsonApiDocumentResult(result)) {
534
+ const document = normalizeJsonApiDocument(result.value);
535
+ if (document.kind === "unknown") {
536
+ throw createJsonApiTransportError(
537
+ 500,
538
+ "JSON:API document result must contain a valid JSON:API document.",
539
+ "jsonapi_document_result_invalid"
540
+ );
541
+ }
542
+
543
+ if (normalizedSuccessKind === "collection" && document.kind !== "collection") {
544
+ throw createJsonApiTransportError(
545
+ 500,
546
+ "JSON:API collection route requires a collection document result.",
547
+ "jsonapi_document_result_kind_mismatch"
548
+ );
549
+ }
550
+
551
+ if (
552
+ (normalizedSuccessKind === "record" || normalizedSuccessKind === "nullable-record") &&
553
+ document.kind !== "resource"
554
+ ) {
555
+ throw createJsonApiTransportError(
556
+ 500,
557
+ "JSON:API record route requires a resource document result.",
558
+ "jsonapi_document_result_kind_mismatch"
559
+ );
560
+ }
561
+
562
+ if (normalizedSuccessKind === "meta" && document.kind !== "meta") {
563
+ throw createJsonApiTransportError(
564
+ 500,
565
+ "JSON:API meta route requires a meta-only document result.",
566
+ "jsonapi_document_result_kind_mismatch"
567
+ );
568
+ }
569
+
570
+ return result.value;
571
+ }
572
+
573
+ if (normalizedSuccessKind === "meta") {
574
+ if (isJsonApiMetaResult(result)) {
575
+ return createJsonApiDocument({
576
+ meta: result.value
577
+ });
578
+ }
579
+
580
+ throw createJsonApiTransportError(
581
+ 500,
582
+ "JSON:API meta route requires a meta result.",
583
+ "jsonapi_meta_result_missing"
584
+ );
585
+ }
586
+
587
+ if (!isJsonApiDataResult(result)) {
588
+ throw createJsonApiTransportError(
589
+ 500,
590
+ "JSON:API resource route requires a data or document result.",
591
+ "jsonapi_success_result_kind_invalid"
592
+ );
593
+ }
594
+
595
+ const sourcePayload = result.value;
596
+
597
+ if (normalizedSuccessKind === "collection") {
598
+ const items = normalizeArray(resolveCollectionItems(sourcePayload, context));
599
+ const documentOptions = {
600
+ data: items.map((entry) => buildResourceObject(entry, context))
601
+ };
602
+
603
+ if (typeof getIncluded === "function") {
604
+ const included = normalizeArray(getIncluded(sourcePayload, context));
605
+ if (included.length > 0) {
606
+ documentOptions.included = included;
607
+ }
608
+ }
609
+
610
+ const links = typeof getDocumentLinks === "function" ? getDocumentLinks(sourcePayload, context) : undefined;
611
+ if (links !== undefined) {
612
+ documentOptions.links = links;
613
+ }
614
+
615
+ const meta = resolveDocumentMeta(sourcePayload, context);
616
+ if (meta !== undefined) {
617
+ documentOptions.meta = meta;
618
+ }
619
+
620
+ return createJsonApiDocument(documentOptions);
621
+ }
622
+
623
+ if (sourcePayload == null) {
624
+ if (normalizedSuccessKind === "nullable-record") {
625
+ return createJsonApiDocument({
626
+ data: null
627
+ });
628
+ }
629
+
630
+ throw createJsonApiTransportError(500, "JSON:API resource response requires a record payload.", "jsonapi_record_missing");
631
+ }
632
+
633
+ const documentOptions = {
634
+ data: buildResourceObject(sourcePayload, context)
635
+ };
636
+
637
+ if (typeof getIncluded === "function") {
638
+ const included = normalizeArray(getIncluded(sourcePayload, context));
639
+ if (included.length > 0) {
640
+ documentOptions.included = included;
641
+ }
642
+ }
643
+
644
+ if (typeof getDocumentLinks === "function") {
645
+ const links = getDocumentLinks(sourcePayload, context);
646
+ if (links !== undefined) {
647
+ documentOptions.links = links;
648
+ }
649
+ }
650
+
651
+ if (typeof getDocumentMeta === "function") {
652
+ const meta = getDocumentMeta(sourcePayload, context);
653
+ if (meta !== undefined) {
654
+ documentOptions.meta = meta;
655
+ }
656
+ }
657
+
658
+ return createJsonApiDocument(documentOptions);
659
+ },
660
+ error(error, {
661
+ statusCode = 500,
662
+ code = ""
663
+ } = {}) {
664
+ const fieldErrors = isRecord(error?.fieldErrors)
665
+ ? error.fieldErrors
666
+ : isRecord(error?.details?.fieldErrors)
667
+ ? error.details.fieldErrors
668
+ : {};
669
+
670
+ return createJsonApiErrorDocumentFromFailure({
671
+ statusCode,
672
+ code,
673
+ message: error?.message,
674
+ fieldErrors,
675
+ validationIssues: Array.isArray(error?.validation) ? error.validation : [],
676
+ validationContext: String(error?.validationContext || "").trim(),
677
+ pointerPrefix
678
+ });
679
+ }
680
+ });
681
+ }
682
+
683
+ function createJsonApiResourceRouteContract({
684
+ type = "",
685
+ requestType = "",
686
+ responseType = "",
687
+ body = null,
688
+ query = null,
689
+ output = null,
690
+ outputKind = "record",
691
+ successStatus = 200,
692
+ includeValidation400 = false,
693
+ allowBodyId = false,
694
+ pointerPrefix = "/data/attributes",
695
+ getRecordType = null,
696
+ getRecordId = null,
697
+ getRecordAttributes = null,
698
+ getRecordRelationships = null,
699
+ getRecordLinks = null,
700
+ getRecordMeta = null,
701
+ getIncluded = null,
702
+ getDocumentLinks = null,
703
+ getDocumentMeta = null,
704
+ getCollectionItems = null,
705
+ mapRequestRelationships = null
706
+ } = {}) {
707
+ const resolvedTypes = resolveRouteTypes({
708
+ type,
709
+ requestType,
710
+ responseType
711
+ });
712
+ const normalizedRequestType = resolvedTypes.requestType;
713
+ const normalizedResponseType = resolvedTypes.responseType;
714
+ const normalizedOutputKind = String(outputKind || "record").trim().toLowerCase();
715
+ const statusCode = Number(successStatus);
716
+
717
+ if (!Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {
718
+ throw new TypeError("JSON:API resource route contract requires a 2xx successStatus.");
719
+ }
720
+
721
+ const transport = createJsonApiResourceRouteTransport({
722
+ requestType: normalizedRequestType,
723
+ responseType: normalizedResponseType,
724
+ query,
725
+ allowBodyId,
726
+ successKind: normalizedOutputKind,
727
+ pointerPrefix,
728
+ getRecordType,
729
+ getRecordId,
730
+ getRecordAttributes,
731
+ getRecordRelationships,
732
+ getRecordLinks,
733
+ getRecordMeta,
734
+ getIncluded,
735
+ getDocumentLinks,
736
+ getDocumentMeta,
737
+ getCollectionItems,
738
+ mapRequestRelationships
739
+ });
740
+ const contract = {
741
+ transport
742
+ };
743
+
744
+ const fastifySchema = {};
745
+ if (body) {
746
+ contract.body = body;
747
+ fastifySchema.body = createJsonApiResourceRequestBodyTransportSchema({
748
+ type: normalizedRequestType,
749
+ attributes: body,
750
+ requireId: allowBodyId
751
+ });
752
+ }
753
+ if (query) {
754
+ contract.query = query;
755
+ fastifySchema.querystring = createJsonApiResourceQueryTransportSchema({
756
+ query,
757
+ responseType: normalizedResponseType
758
+ });
759
+ }
760
+
761
+ if (Object.keys(fastifySchema).length > 0) {
762
+ contract.advanced = {
763
+ fastifySchema
764
+ };
765
+ }
766
+
767
+ const successResponses = {};
768
+ if (normalizedOutputKind !== "no-content") {
769
+ if (!output) {
770
+ throw new TypeError(`JSON:API resource route contract for ${normalizedResponseType} requires output schema.`);
771
+ }
772
+
773
+ successResponses[statusCode] = createTransportResponseSchema(
774
+ createJsonApiResourceSuccessTransportSchema({
775
+ type: normalizedResponseType,
776
+ attributes: output,
777
+ kind: normalizedOutputKind,
778
+ includeLinks: typeof getDocumentLinks === "function",
779
+ includeMeta: typeof getDocumentMeta === "function" || normalizedOutputKind === "collection",
780
+ includeIncluded: typeof getIncluded === "function"
781
+ })
782
+ );
783
+ }
784
+
785
+ contract.responses = withJsonApiErrorResponses(successResponses, {
786
+ includeValidation400
787
+ });
788
+
789
+ return Object.freeze(contract);
790
+ }
791
+
792
+ export {
793
+ JSON_API_ERROR_DOCUMENT_SCHEMA,
794
+ createJsonApiResourceObjectTransportSchema,
795
+ createJsonApiResourceRequestBodyTransportSchema,
796
+ createJsonApiResourceSuccessTransportSchema,
797
+ withJsonApiErrorResponses,
798
+ createJsonApiResourceRouteTransport,
799
+ createJsonApiResourceRouteContract
800
+ };