@jskit-ai/http-runtime 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +2 -4
- package/package.json +5 -5
- package/src/shared/clientRuntime/client.js +126 -9
- package/src/shared/clientRuntime/errors.js +6 -0
- package/src/shared/clientRuntime/jsonApiResourceTransport.js +241 -0
- package/src/shared/index.js +54 -5
- package/src/shared/validators/command.js +5 -4
- package/src/shared/validators/errorResponses.js +125 -62
- package/src/shared/validators/httpValidatorsApi.js +83 -12
- package/src/shared/validators/jsonApiQueryTransport.js +211 -0
- package/src/shared/validators/jsonApiResponses.js +3 -0
- package/src/shared/validators/jsonApiResult.js +83 -0
- package/src/shared/validators/jsonApiRouteTransport.js +800 -0
- package/src/shared/validators/jsonApiTransport.js +484 -0
- package/src/shared/validators/operationValidation.js +62 -101
- package/src/shared/validators/paginationQuery.js +14 -19
- package/src/shared/validators/resource.js +15 -17
- package/src/shared/validators/schemaUtils.js +18 -5
- package/src/shared/validators/transportSchemaEmbedding.js +81 -0
- package/test/client.test.js +279 -0
- package/test/command.test.js +38 -21
- package/test/entrypoints.boundary.test.js +8 -0
- package/test/errorResponses.test.js +49 -13
- package/test/jsonApiRouteTransport.test.js +349 -0
- package/test/jsonApiTransport.test.js +231 -0
- package/test/operationMessages.test.js +115 -66
- package/test/operationValidation.test.js +147 -159
- package/test/paginationQuery.test.js +4 -8
- package/test/resource.test.js +89 -55
- package/test/validationErrors.test.js +33 -0
- package/src/shared/validators/typeboxFormats.js +0 -43
- package/test/typeboxFormats.test.js +0 -42
|
@@ -1,62 +1,114 @@
|
|
|
1
|
-
import {
|
|
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
|
|
5
|
+
const fieldErrorsFieldDefinition = deepFreeze({
|
|
6
|
+
type: "object",
|
|
7
|
+
values: {
|
|
8
|
+
type: "string",
|
|
9
|
+
minLength: 1
|
|
10
|
+
}
|
|
11
|
+
});
|
|
4
12
|
|
|
5
|
-
const apiErrorDetailsSchema =
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
additionalProperties: true
|
|
13
|
+
const apiErrorDetailsSchema = createSchema({
|
|
14
|
+
fieldErrors: {
|
|
15
|
+
...fieldErrorsFieldDefinition,
|
|
16
|
+
required: false
|
|
11
17
|
}
|
|
12
|
-
);
|
|
18
|
+
});
|
|
13
19
|
|
|
14
|
-
const
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
27
|
-
{
|
|
28
|
-
error:
|
|
29
|
-
code:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
152
|
+
return {
|
|
153
|
+
anyOf: values.map((value) => ({ const: value }))
|
|
154
|
+
};
|
|
96
155
|
}
|
|
97
156
|
|
|
98
157
|
export {
|
|
99
|
-
|
|
158
|
+
fieldErrorsFieldDefinition,
|
|
100
159
|
apiErrorDetailsSchema,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
35
|
-
__testables,
|
|
36
|
-
fieldErrorsSchema,
|
|
73
|
+
fieldErrorsFieldDefinition,
|
|
37
74
|
apiErrorDetailsSchema,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,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
|
+
};
|