@jskit-ai/http-runtime 0.1.53 → 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,62 +1,114 @@
1
- import { Type } from "@fastify/type-provider-typebox";
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
+ import { createEmbeddableTransportSchemaDocument } from "./transportSchemaEmbedding.js";
2
4
 
3
- const fieldErrorsSchema = Type.Record(Type.String(), Type.String());
5
+ const fieldErrorsFieldDefinition = deepFreeze({
6
+ type: "object",
7
+ values: {
8
+ type: "string",
9
+ minLength: 1
10
+ }
11
+ });
4
12
 
5
- const apiErrorDetailsSchema = Type.Object(
6
- {
7
- fieldErrors: Type.Optional(fieldErrorsSchema)
8
- },
9
- {
10
- additionalProperties: true
13
+ const apiErrorDetailsSchema = createSchema({
14
+ fieldErrors: {
15
+ ...fieldErrorsFieldDefinition,
16
+ required: false
11
17
  }
12
- );
18
+ });
13
19
 
14
- const apiErrorResponseSchema = Type.Object(
15
- {
16
- error: Type.String({ minLength: 1 }),
17
- code: Type.Optional(Type.String({ minLength: 1 })),
18
- details: Type.Optional(apiErrorDetailsSchema),
19
- fieldErrors: Type.Optional(fieldErrorsSchema)
20
- },
21
- {
22
- additionalProperties: false
20
+ const apiValidationErrorDetailsSchema = createSchema({
21
+ fieldErrors: {
22
+ ...fieldErrorsFieldDefinition,
23
+ required: true
23
24
  }
24
- );
25
+ });
25
26
 
26
- const apiValidationErrorResponseSchema = Type.Object(
27
- {
28
- error: Type.String({ minLength: 1 }),
29
- code: Type.Optional(Type.String({ minLength: 1 })),
30
- fieldErrors: fieldErrorsSchema,
31
- details: Type.Object(
32
- {
33
- fieldErrors: fieldErrorsSchema
34
- },
35
- {
36
- additionalProperties: true
27
+ const apiErrorOutputValidator = deepFreeze({
28
+ schema: createSchema({
29
+ error: { type: "string", required: true, minLength: 1 },
30
+ code: { type: "string", required: false, minLength: 1 },
31
+ details: {
32
+ type: "object",
33
+ required: false,
34
+ schema: apiErrorDetailsSchema,
35
+ additionalProperties: true
36
+ },
37
+ fieldErrors: {
38
+ ...fieldErrorsFieldDefinition,
39
+ required: false
40
+ }
41
+ }),
42
+ mode: "replace"
43
+ });
44
+
45
+ const apiValidationErrorOutputValidator = deepFreeze({
46
+ schema: createSchema({
47
+ error: { type: "string", required: true, minLength: 1 },
48
+ code: { type: "string", required: false, minLength: 1 },
49
+ fieldErrors: {
50
+ ...fieldErrorsFieldDefinition,
51
+ required: true
52
+ },
53
+ details: {
54
+ type: "object",
55
+ required: true,
56
+ schema: apiValidationErrorDetailsSchema,
57
+ additionalProperties: true
58
+ }
59
+ }),
60
+ mode: "replace"
61
+ });
62
+
63
+ const apiErrorTransportSchema = apiErrorOutputValidator.schema.toJsonSchema({
64
+ mode: apiErrorOutputValidator.mode
65
+ });
66
+
67
+ const apiValidationErrorTransportSchema = apiValidationErrorOutputValidator.schema.toJsonSchema({
68
+ mode: apiValidationErrorOutputValidator.mode
69
+ });
70
+
71
+ const fastifyDefaultErrorTransportSchema = {
72
+ type: "object",
73
+ additionalProperties: true,
74
+ required: ["statusCode", "error", "message"],
75
+ properties: {
76
+ statusCode: { type: "integer", minimum: 400, maximum: 599 },
77
+ error: { type: "string", minLength: 1 },
78
+ message: { type: "string", minLength: 1 },
79
+ code: { type: "string", minLength: 1 },
80
+ details: {},
81
+ fieldErrors: {
82
+ type: "object",
83
+ additionalProperties: {
84
+ type: "string"
37
85
  }
38
- )
39
- },
40
- {
41
- additionalProperties: false
86
+ }
42
87
  }
88
+ };
89
+
90
+ const STANDARD_ERROR_STATUS_CODES = [400, 401, 403, 404, 409, 422, 429, 500, 503];
91
+
92
+ function createTransportResponseSchema(schema = {}) {
93
+ return {
94
+ transportSchema: schema
95
+ };
96
+ }
97
+
98
+ const embeddedApiErrorTransportSchema = createEmbeddableTransportSchemaDocument(
99
+ apiErrorTransportSchema,
100
+ "ApiErrorOutput"
43
101
  );
44
102
 
45
- const fastifyDefaultErrorResponseSchema = Type.Object(
46
- {
47
- statusCode: Type.Integer({ minimum: 400, maximum: 599 }),
48
- error: Type.String({ minLength: 1 }),
49
- message: Type.String({ minLength: 1 }),
50
- code: Type.Optional(Type.String({ minLength: 1 })),
51
- details: Type.Optional(Type.Unknown()),
52
- fieldErrors: Type.Optional(fieldErrorsSchema)
53
- },
54
- {
55
- additionalProperties: true
56
- }
103
+ const embeddedApiValidationErrorTransportSchema = createEmbeddableTransportSchemaDocument(
104
+ apiValidationErrorTransportSchema,
105
+ "ApiValidationErrorOutput"
57
106
  );
58
107
 
59
- const STANDARD_ERROR_STATUS_CODES = [400, 401, 403, 404, 409, 422, 429, 500, 503];
108
+ const sharedErrorTransportDefinitions = {
109
+ ...embeddedApiValidationErrorTransportSchema.definitions,
110
+ ...embeddedApiErrorTransportSchema.definitions
111
+ };
60
112
 
61
113
  function passthroughErrorResponses(successResponses) {
62
114
  return successResponses;
@@ -73,35 +125,46 @@ function withStandardErrorResponses(successResponses, { includeValidation400 = f
73
125
  }
74
126
 
75
127
  if (statusCode === 400 && includeValidation400) {
76
- responses[statusCode] = {
77
- schema: Type.Union([
78
- apiValidationErrorResponseSchema,
79
- apiErrorResponseSchema,
80
- fastifyDefaultErrorResponseSchema
81
- ])
82
- };
128
+ responses[statusCode] = createTransportResponseSchema({
129
+ anyOf: [
130
+ embeddedApiValidationErrorTransportSchema.schema,
131
+ embeddedApiErrorTransportSchema.schema,
132
+ fastifyDefaultErrorTransportSchema
133
+ ],
134
+ definitions: sharedErrorTransportDefinitions
135
+ });
83
136
  continue;
84
137
  }
85
138
 
86
- responses[statusCode] = {
87
- schema: Type.Union([apiErrorResponseSchema, fastifyDefaultErrorResponseSchema])
88
- };
139
+ responses[statusCode] = createTransportResponseSchema({
140
+ anyOf: [
141
+ embeddedApiErrorTransportSchema.schema,
142
+ fastifyDefaultErrorTransportSchema
143
+ ],
144
+ definitions: embeddedApiErrorTransportSchema.definitions
145
+ });
89
146
  }
90
147
 
91
148
  return responses;
92
149
  }
93
150
 
94
151
  function enumSchema(values) {
95
- return Type.Union(values.map((value) => Type.Literal(value)));
152
+ return {
153
+ anyOf: values.map((value) => ({ const: value }))
154
+ };
96
155
  }
97
156
 
98
157
  export {
99
- fieldErrorsSchema,
158
+ fieldErrorsFieldDefinition,
100
159
  apiErrorDetailsSchema,
101
- apiErrorResponseSchema,
102
- apiValidationErrorResponseSchema,
103
- fastifyDefaultErrorResponseSchema,
160
+ apiValidationErrorDetailsSchema,
161
+ apiErrorOutputValidator,
162
+ apiValidationErrorOutputValidator,
163
+ apiErrorTransportSchema,
164
+ apiValidationErrorTransportSchema,
165
+ fastifyDefaultErrorTransportSchema,
104
166
  STANDARD_ERROR_STATUS_CODES,
167
+ createTransportResponseSchema,
105
168
  passthroughErrorResponses,
106
169
  withStandardErrorResponses,
107
170
  enumSchema
@@ -1,12 +1,15 @@
1
1
  import { createPaginationQuerySchema } from "./paginationQuery.js";
2
- import { registerTypeBoxFormats, __testables } from "./typeboxFormats.js";
3
2
  import {
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,17 +31,55 @@ import {
28
31
  validateOperationSection,
29
32
  validateOperationInput
30
33
  } from "./operationValidation.js";
34
+ import {
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 "./jsonApiTransport.js";
50
+ import {
51
+ JSON_API_QUERY_PAGE_CURSOR_KEY,
52
+ JSON_API_QUERY_PAGE_LIMIT_KEY,
53
+ JSON_API_QUERY_INCLUDE_KEY,
54
+ JSON_API_QUERY_SORT_KEY,
55
+ mapPlainQueryKeyToTransportKey,
56
+ mapTransportQueryKeyToPlainKey,
57
+ encodeJsonApiResourceQueryObject,
58
+ decodeJsonApiResourceQueryObject,
59
+ createJsonApiResourceQueryTransportSchema
60
+ } from "./jsonApiQueryTransport.js";
61
+ import {
62
+ JSON_API_ERROR_DOCUMENT_SCHEMA,
63
+ createJsonApiResourceObjectTransportSchema,
64
+ createJsonApiResourceRequestBodyTransportSchema,
65
+ createJsonApiResourceSuccessTransportSchema,
66
+ withJsonApiErrorResponses,
67
+ createJsonApiResourceRouteTransport,
68
+ createJsonApiResourceRouteContract
69
+ } from "./jsonApiRouteTransport.js";
31
70
 
32
71
  const HTTP_VALIDATORS_API = Object.freeze({
33
72
  createPaginationQuerySchema,
34
- registerTypeBoxFormats,
35
- __testables,
36
- fieldErrorsSchema,
73
+ fieldErrorsFieldDefinition,
37
74
  apiErrorDetailsSchema,
38
- apiErrorResponseSchema,
39
- apiValidationErrorResponseSchema,
40
- fastifyDefaultErrorResponseSchema,
75
+ apiValidationErrorDetailsSchema,
76
+ apiErrorOutputValidator,
77
+ apiValidationErrorOutputValidator,
78
+ apiErrorTransportSchema,
79
+ apiValidationErrorTransportSchema,
80
+ fastifyDefaultErrorTransportSchema,
41
81
  STANDARD_ERROR_STATUS_CODES,
82
+ createTransportResponseSchema,
42
83
  passthroughErrorResponses,
43
84
  withStandardErrorResponses,
44
85
  enumSchema,
@@ -52,7 +93,37 @@ const HTTP_VALIDATORS_API = Object.freeze({
52
93
  resolveIssueMessageFromSchema,
53
94
  mapOperationIssues,
54
95
  validateOperationSection,
55
- validateOperationInput
96
+ validateOperationInput,
97
+ JSON_API_CONTENT_TYPE,
98
+ createJsonApiDocument,
99
+ createJsonApiErrorDocumentFromFailure,
100
+ createJsonApiErrorObject,
101
+ createJsonApiResourceObject,
102
+ isJsonApiCollectionDocument,
103
+ isJsonApiContentType,
104
+ isJsonApiErrorDocument,
105
+ isJsonApiResourceDocument,
106
+ isJsonContentType,
107
+ normalizeJsonApiDocument,
108
+ normalizeJsonApiResourceObject,
109
+ resolveJsonApiTransportTypes,
110
+ simplifyJsonApiDocument,
111
+ JSON_API_QUERY_PAGE_CURSOR_KEY,
112
+ JSON_API_QUERY_PAGE_LIMIT_KEY,
113
+ JSON_API_QUERY_INCLUDE_KEY,
114
+ JSON_API_QUERY_SORT_KEY,
115
+ mapPlainQueryKeyToTransportKey,
116
+ mapTransportQueryKeyToPlainKey,
117
+ encodeJsonApiResourceQueryObject,
118
+ decodeJsonApiResourceQueryObject,
119
+ createJsonApiResourceQueryTransportSchema,
120
+ JSON_API_ERROR_DOCUMENT_SCHEMA,
121
+ createJsonApiResourceObjectTransportSchema,
122
+ createJsonApiResourceRequestBodyTransportSchema,
123
+ createJsonApiResourceSuccessTransportSchema,
124
+ withJsonApiErrorResponses,
125
+ createJsonApiResourceRouteTransport,
126
+ createJsonApiResourceRouteContract
56
127
  });
57
128
 
58
129
  export { HTTP_VALIDATORS_API };
@@ -0,0 +1,211 @@
1
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
3
+
4
+ const JSON_API_QUERY_PAGE_CURSOR_KEY = "page[cursor]";
5
+ const JSON_API_QUERY_PAGE_LIMIT_KEY = "page[limit]";
6
+ const JSON_API_QUERY_INCLUDE_KEY = "include";
7
+ const JSON_API_QUERY_SORT_KEY = "sort";
8
+ const JSON_API_FILTER_PREFIX = "filter[";
9
+ const JSON_API_FIELDS_PREFIX = "fields[";
10
+
11
+ function isRecord(value) {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+
15
+ function normalizeQueryKey(key = "") {
16
+ return String(key || "").trim();
17
+ }
18
+
19
+ function buildFieldsTransportKey(responseType = "") {
20
+ const normalizedResponseType = normalizeText(responseType);
21
+ return normalizedResponseType ? `${JSON_API_FIELDS_PREFIX}${normalizedResponseType}]` : "";
22
+ }
23
+
24
+ function mapPlainQueryKeyToTransportKey(key = "", { responseType = "" } = {}) {
25
+ const normalizedKey = normalizeQueryKey(key);
26
+ if (!normalizedKey) {
27
+ return "";
28
+ }
29
+
30
+ if (normalizedKey === "cursor") {
31
+ return JSON_API_QUERY_PAGE_CURSOR_KEY;
32
+ }
33
+ if (normalizedKey === "limit") {
34
+ return JSON_API_QUERY_PAGE_LIMIT_KEY;
35
+ }
36
+ if (normalizedKey === "q") {
37
+ return `${JSON_API_FILTER_PREFIX}q]`;
38
+ }
39
+ if (normalizedKey === "include") {
40
+ return JSON_API_QUERY_INCLUDE_KEY;
41
+ }
42
+ if (normalizedKey === "sort") {
43
+ return JSON_API_QUERY_SORT_KEY;
44
+ }
45
+ if (normalizedKey === "fields") {
46
+ return buildFieldsTransportKey(responseType);
47
+ }
48
+
49
+ return `${JSON_API_FILTER_PREFIX}${normalizedKey}]`;
50
+ }
51
+
52
+ function mapTransportQueryKeyToPlainKey(key = "", { responseType = "" } = {}) {
53
+ const normalizedKey = normalizeQueryKey(key);
54
+ if (!normalizedKey) {
55
+ return "";
56
+ }
57
+
58
+ if (normalizedKey === JSON_API_QUERY_PAGE_CURSOR_KEY) {
59
+ return "cursor";
60
+ }
61
+ if (normalizedKey === JSON_API_QUERY_PAGE_LIMIT_KEY) {
62
+ return "limit";
63
+ }
64
+ if (normalizedKey === JSON_API_QUERY_INCLUDE_KEY) {
65
+ return "include";
66
+ }
67
+ if (normalizedKey === JSON_API_QUERY_SORT_KEY) {
68
+ return "sort";
69
+ }
70
+ if (normalizedKey === `${JSON_API_FILTER_PREFIX}q]`) {
71
+ return "q";
72
+ }
73
+
74
+ const fieldsTransportKey = buildFieldsTransportKey(responseType);
75
+ if (fieldsTransportKey && normalizedKey === fieldsTransportKey) {
76
+ return "fields";
77
+ }
78
+
79
+ if (normalizedKey.startsWith(JSON_API_FILTER_PREFIX) && normalizedKey.endsWith("]")) {
80
+ return normalizedKey.slice(JSON_API_FILTER_PREFIX.length, -1);
81
+ }
82
+
83
+ if (normalizedKey.startsWith(JSON_API_FIELDS_PREFIX) && normalizedKey.endsWith("]")) {
84
+ return "fields";
85
+ }
86
+
87
+ return normalizedKey;
88
+ }
89
+
90
+ function normalizeTransportQueryScalar(value) {
91
+ if (Array.isArray(value)) {
92
+ const normalizedValues = value
93
+ .map((entry) => String(entry ?? "").trim())
94
+ .filter(Boolean);
95
+ if (normalizedValues.length < 1) {
96
+ return "";
97
+ }
98
+ return normalizedValues.join(",");
99
+ }
100
+
101
+ return String(value ?? "").trim();
102
+ }
103
+
104
+ function encodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}) {
105
+ if (!isRecord(query)) {
106
+ return Object.freeze({});
107
+ }
108
+
109
+ const source = normalizeObject(query);
110
+ const encoded = {};
111
+
112
+ for (const [rawKey, rawValue] of Object.entries(source)) {
113
+ const transportKey = mapPlainQueryKeyToTransportKey(rawKey, {
114
+ responseType
115
+ });
116
+ if (!transportKey) {
117
+ continue;
118
+ }
119
+
120
+ const values = Array.isArray(rawValue) ? rawValue : [rawValue];
121
+ const normalizedValues = values
122
+ .map((entry) => String(entry ?? "").trim())
123
+ .filter(Boolean);
124
+ if (normalizedValues.length < 1) {
125
+ continue;
126
+ }
127
+
128
+ encoded[transportKey] = normalizedValues.length === 1 ? normalizedValues[0] : normalizedValues;
129
+ }
130
+
131
+ return Object.freeze(encoded);
132
+ }
133
+
134
+ function decodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}) {
135
+ if (!isRecord(query)) {
136
+ return Object.freeze({});
137
+ }
138
+
139
+ const source = normalizeObject(query);
140
+ const decoded = {};
141
+
142
+ for (const [rawKey, rawValue] of Object.entries(source)) {
143
+ const plainKey = mapTransportQueryKeyToPlainKey(rawKey, {
144
+ responseType
145
+ });
146
+ if (!plainKey) {
147
+ continue;
148
+ }
149
+
150
+ const normalizedValue = normalizeTransportQueryScalar(rawValue);
151
+ if (!normalizedValue) {
152
+ continue;
153
+ }
154
+
155
+ decoded[plainKey] = normalizedValue;
156
+ }
157
+
158
+ return Object.freeze(decoded);
159
+ }
160
+
161
+ function createJsonApiResourceQueryTransportSchema({
162
+ query,
163
+ responseType = ""
164
+ } = {}) {
165
+ const transportSchema = resolveSchemaTransportSchemaDefinition(query, {
166
+ context: "JSON:API resource query",
167
+ defaultMode: "patch"
168
+ });
169
+
170
+ if (!transportSchema || typeof transportSchema !== "object" || Array.isArray(transportSchema)) {
171
+ throw new TypeError("JSON:API resource query transport schema must resolve to an object schema.");
172
+ }
173
+
174
+ const sourceSchema = normalizeObject(transportSchema);
175
+ const sourceProperties = normalizeObject(sourceSchema.properties);
176
+ const properties = {};
177
+
178
+ for (const [plainKey, propertySchema] of Object.entries(sourceProperties)) {
179
+ const transportKey = mapPlainQueryKeyToTransportKey(plainKey, {
180
+ responseType
181
+ });
182
+ if (!transportKey) {
183
+ continue;
184
+ }
185
+ properties[transportKey] = propertySchema;
186
+ }
187
+
188
+ const schema = {
189
+ type: "object",
190
+ additionalProperties: false,
191
+ properties
192
+ };
193
+
194
+ if (isRecord(sourceSchema.definitions) && Object.keys(sourceSchema.definitions).length > 0) {
195
+ schema.definitions = normalizeObject(sourceSchema.definitions);
196
+ }
197
+
198
+ return schema;
199
+ }
200
+
201
+ export {
202
+ JSON_API_QUERY_PAGE_CURSOR_KEY,
203
+ JSON_API_QUERY_PAGE_LIMIT_KEY,
204
+ JSON_API_QUERY_INCLUDE_KEY,
205
+ JSON_API_QUERY_SORT_KEY,
206
+ mapPlainQueryKeyToTransportKey,
207
+ mapTransportQueryKeyToPlainKey,
208
+ encodeJsonApiResourceQueryObject,
209
+ decodeJsonApiResourceQueryObject,
210
+ createJsonApiResourceQueryTransportSchema
211
+ };
@@ -0,0 +1,3 @@
1
+ export {
2
+ simplifyJsonApiDocument
3
+ } from "./jsonApiTransport.js";
@@ -0,0 +1,83 @@
1
+ const JSON_API_RESULT_MARKER = "__jskitJsonApiResult";
2
+ const JSON_API_RESULT_KINDS = Object.freeze([
3
+ "data",
4
+ "document",
5
+ "meta"
6
+ ]);
7
+
8
+ function normalizeJsonApiResultKind(kind = "") {
9
+ const normalizedKind = String(kind || "").trim().toLowerCase();
10
+ if (!JSON_API_RESULT_KINDS.includes(normalizedKind)) {
11
+ throw new TypeError(`Unsupported JSON:API result kind: ${normalizedKind || "<empty>"}.`);
12
+ }
13
+
14
+ return normalizedKind;
15
+ }
16
+
17
+ function createJsonApiResult(kind, value) {
18
+ return Object.freeze({
19
+ [JSON_API_RESULT_MARKER]: true,
20
+ kind: normalizeJsonApiResultKind(kind),
21
+ value
22
+ });
23
+ }
24
+
25
+ function isJsonApiResult(value) {
26
+ return Boolean(value) &&
27
+ typeof value === "object" &&
28
+ !Array.isArray(value) &&
29
+ value[JSON_API_RESULT_MARKER] === true &&
30
+ JSON_API_RESULT_KINDS.includes(String(value.kind || "").trim().toLowerCase());
31
+ }
32
+
33
+ function isJsonApiResultKind(value, kind) {
34
+ return isJsonApiResult(value) && value.kind === normalizeJsonApiResultKind(kind);
35
+ }
36
+
37
+ function returnJsonApiData(data) {
38
+ return createJsonApiResult("data", data);
39
+ }
40
+
41
+ function returnJsonApiDocument(document) {
42
+ return createJsonApiResult("document", document);
43
+ }
44
+
45
+ function returnJsonApiMeta(meta) {
46
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
47
+ throw new TypeError("returnJsonApiMeta requires a meta object.");
48
+ }
49
+
50
+ return createJsonApiResult("meta", meta);
51
+ }
52
+
53
+ function isJsonApiDataResult(value) {
54
+ return isJsonApiResultKind(value, "data");
55
+ }
56
+
57
+ function isJsonApiDocumentResult(value) {
58
+ return isJsonApiResultKind(value, "document");
59
+ }
60
+
61
+ function isJsonApiMetaResult(value) {
62
+ return isJsonApiResultKind(value, "meta");
63
+ }
64
+
65
+ function unwrapJsonApiResult(value) {
66
+ if (!isJsonApiResult(value)) {
67
+ return null;
68
+ }
69
+
70
+ return value;
71
+ }
72
+
73
+ export {
74
+ JSON_API_RESULT_MARKER,
75
+ returnJsonApiData,
76
+ returnJsonApiDocument,
77
+ returnJsonApiMeta,
78
+ isJsonApiResult,
79
+ isJsonApiDataResult,
80
+ isJsonApiDocumentResult,
81
+ isJsonApiMetaResult,
82
+ unwrapJsonApiResult
83
+ };