@jskit-ai/http-runtime 0.1.133 → 0.1.135
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
"packageVersion": 1,
|
|
3
3
|
"packageId": "@jskit-ai/http-runtime",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.135",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"dependsOn": [],
|
|
7
7
|
"capabilities": {
|
|
@@ -67,7 +67,7 @@ export default Object.freeze({
|
|
|
67
67
|
"mutations": {
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"runtime": {
|
|
70
|
-
"@jskit-ai/kernel": "0.1.
|
|
70
|
+
"@jskit-ai/kernel": "0.1.137"
|
|
71
71
|
},
|
|
72
72
|
"dev": {}
|
|
73
73
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/http-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.135",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"./shared/validators/operationValidation": "./src/shared/validators/operationValidation.js"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@jskit-ai/kernel": "0.1.
|
|
21
|
+
"@jskit-ai/kernel": "0.1.137",
|
|
22
22
|
"json-rest-schema": "1.x.x"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import {
|
|
3
|
+
normalizeJsonApiFieldList,
|
|
4
|
+
normalizeJsonApiFieldsets
|
|
5
|
+
} from "@jskit-ai/kernel/shared/support/jsonApiFieldsets";
|
|
2
6
|
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
3
7
|
|
|
4
8
|
const JSON_API_QUERY_PAGE_CURSOR_KEY = "page[cursor]";
|
|
@@ -21,6 +25,50 @@ function buildFieldsTransportKey(responseType = "") {
|
|
|
21
25
|
return normalizedResponseType ? `${JSON_API_FIELDS_PREFIX}${normalizedResponseType}]` : "";
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function parseFieldsTransportType(key = "") {
|
|
29
|
+
const normalizedKey = normalizeQueryKey(key);
|
|
30
|
+
if (!normalizedKey.startsWith(JSON_API_FIELDS_PREFIX) || !normalizedKey.endsWith("]")) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return normalizeText(normalizedKey.slice(JSON_API_FIELDS_PREFIX.length, -1));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function addJsonApiFieldset(fieldsets, type = "", value = []) {
|
|
38
|
+
const normalizedType = normalizeText(type);
|
|
39
|
+
const fields = normalizeJsonApiFieldList(value);
|
|
40
|
+
if (!normalizedType || fields.length < 1) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fieldsets.set(
|
|
45
|
+
normalizedType,
|
|
46
|
+
normalizeJsonApiFieldList([
|
|
47
|
+
...(fieldsets.get(normalizedType) || []),
|
|
48
|
+
...fields
|
|
49
|
+
])
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createJsonApiFieldsTransportValueSchema() {
|
|
54
|
+
return {
|
|
55
|
+
anyOf: [
|
|
56
|
+
{
|
|
57
|
+
type: "string",
|
|
58
|
+
minLength: 1
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: "array",
|
|
62
|
+
minItems: 1,
|
|
63
|
+
items: {
|
|
64
|
+
type: "string",
|
|
65
|
+
minLength: 1
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
24
72
|
function mapPlainQueryKeyToTransportKey(key = "", { responseType = "" } = {}) {
|
|
25
73
|
const normalizedKey = normalizeQueryKey(key);
|
|
26
74
|
if (!normalizedKey) {
|
|
@@ -110,6 +158,15 @@ function encodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}
|
|
|
110
158
|
const encoded = {};
|
|
111
159
|
|
|
112
160
|
for (const [rawKey, rawValue] of Object.entries(source)) {
|
|
161
|
+
if (normalizeQueryKey(rawKey) === "fields") {
|
|
162
|
+
for (const [type, fields] of Object.entries(normalizeJsonApiFieldsets(rawValue, {
|
|
163
|
+
primaryType: responseType
|
|
164
|
+
}))) {
|
|
165
|
+
encoded[buildFieldsTransportKey(type)] = fields.join(",");
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
113
170
|
const transportKey = mapPlainQueryKeyToTransportKey(rawKey, {
|
|
114
171
|
responseType
|
|
115
172
|
});
|
|
@@ -138,8 +195,20 @@ function decodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}
|
|
|
138
195
|
|
|
139
196
|
const source = normalizeObject(query);
|
|
140
197
|
const decoded = {};
|
|
198
|
+
const fieldsets = new Map();
|
|
141
199
|
|
|
142
200
|
for (const [rawKey, rawValue] of Object.entries(source)) {
|
|
201
|
+
if (normalizeQueryKey(rawKey) === "fields") {
|
|
202
|
+
addJsonApiFieldset(fieldsets, responseType, rawValue);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fieldsetType = parseFieldsTransportType(rawKey);
|
|
207
|
+
if (fieldsetType) {
|
|
208
|
+
addJsonApiFieldset(fieldsets, fieldsetType, rawValue);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
143
212
|
const plainKey = mapTransportQueryKeyToPlainKey(rawKey, {
|
|
144
213
|
responseType
|
|
145
214
|
});
|
|
@@ -155,6 +224,11 @@ function decodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}
|
|
|
155
224
|
decoded[plainKey] = normalizedValue;
|
|
156
225
|
}
|
|
157
226
|
|
|
227
|
+
const normalizedFieldsets = normalizeJsonApiFieldsets(Object.fromEntries(fieldsets));
|
|
228
|
+
if (Object.keys(normalizedFieldsets).length > 0) {
|
|
229
|
+
decoded.fields = normalizedFieldsets;
|
|
230
|
+
}
|
|
231
|
+
|
|
158
232
|
return Object.freeze(decoded);
|
|
159
233
|
}
|
|
160
234
|
|
|
@@ -174,8 +248,16 @@ function createJsonApiResourceQueryTransportSchema({
|
|
|
174
248
|
const sourceSchema = normalizeObject(transportSchema);
|
|
175
249
|
const sourceProperties = normalizeObject(sourceSchema.properties);
|
|
176
250
|
const properties = {};
|
|
251
|
+
const patternProperties = {};
|
|
177
252
|
|
|
178
253
|
for (const [plainKey, propertySchema] of Object.entries(sourceProperties)) {
|
|
254
|
+
if (plainKey === "fields") {
|
|
255
|
+
properties.fields = createJsonApiFieldsTransportValueSchema();
|
|
256
|
+
patternProperties["^fields\\[[^\\[\\]]+\\]$"] =
|
|
257
|
+
createJsonApiFieldsTransportValueSchema();
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
179
261
|
const transportKey = mapPlainQueryKeyToTransportKey(plainKey, {
|
|
180
262
|
responseType
|
|
181
263
|
});
|
|
@@ -190,6 +272,9 @@ function createJsonApiResourceQueryTransportSchema({
|
|
|
190
272
|
additionalProperties: false,
|
|
191
273
|
properties
|
|
192
274
|
};
|
|
275
|
+
if (Object.keys(patternProperties).length > 0) {
|
|
276
|
+
schema.patternProperties = patternProperties;
|
|
277
|
+
}
|
|
193
278
|
|
|
194
279
|
if (isRecord(sourceSchema.definitions) && Object.keys(sourceSchema.definitions).length > 0) {
|
|
195
280
|
schema.definitions = normalizeObject(sourceSchema.definitions);
|
|
@@ -169,7 +169,8 @@ function resolveEmbeddedAttributesTransportSchema(definition, {
|
|
|
169
169
|
context = "JSON:API resource",
|
|
170
170
|
defaultMode = "replace",
|
|
171
171
|
removeId = false,
|
|
172
|
-
removeKeys = []
|
|
172
|
+
removeKeys = [],
|
|
173
|
+
allowSparseFields = false
|
|
173
174
|
} = {}) {
|
|
174
175
|
const transportSchema = resolveSchemaTransportSchemaDefinition(definition, {
|
|
175
176
|
context,
|
|
@@ -204,7 +205,7 @@ function resolveEmbeddedAttributesTransportSchema(definition, {
|
|
|
204
205
|
return isRecord(fieldDefinitions) ? fieldDefinitions : {};
|
|
205
206
|
})();
|
|
206
207
|
|
|
207
|
-
const required = Array.isArray(sourceSchema.required)
|
|
208
|
+
const required = allowSparseFields !== true && Array.isArray(sourceSchema.required)
|
|
208
209
|
? sourceSchema.required.filter((entry) => {
|
|
209
210
|
const normalizedEntry = String(entry || "").trim();
|
|
210
211
|
if (!normalizedEntry || excludedKeys.has(normalizedEntry)) {
|
|
@@ -341,7 +342,8 @@ function createJsonApiResourceObjectTransportSchema({
|
|
|
341
342
|
includeMeta = false,
|
|
342
343
|
excludeAttributeKeys = [],
|
|
343
344
|
relationshipEntries = [],
|
|
344
|
-
relationshipMembersRequired = false
|
|
345
|
+
relationshipMembersRequired = false,
|
|
346
|
+
allowSparseFields = false
|
|
345
347
|
} = {}) {
|
|
346
348
|
const normalizedType = resolveRouteType(type);
|
|
347
349
|
const relationshipsTransport = createJsonApiRelationshipsTransportSchema(relationshipEntries, {
|
|
@@ -357,7 +359,8 @@ function createJsonApiResourceObjectTransportSchema({
|
|
|
357
359
|
removeKeys: [
|
|
358
360
|
...normalizeArray(excludeAttributeKeys),
|
|
359
361
|
...relationFieldKeys,
|
|
360
|
-
]
|
|
362
|
+
],
|
|
363
|
+
allowSparseFields
|
|
361
364
|
});
|
|
362
365
|
|
|
363
366
|
const properties = {
|
|
@@ -366,7 +369,7 @@ function createJsonApiResourceObjectTransportSchema({
|
|
|
366
369
|
},
|
|
367
370
|
attributes: embeddedAttributes.schema
|
|
368
371
|
};
|
|
369
|
-
const required = ["type", "attributes"];
|
|
372
|
+
const required = allowSparseFields === true ? ["type"] : ["type", "attributes"];
|
|
370
373
|
|
|
371
374
|
if (requireId) {
|
|
372
375
|
properties.id = JSON_API_ID_SCHEMA;
|
|
@@ -457,6 +460,7 @@ function createJsonApiResourceSuccessTransportSchema({
|
|
|
457
460
|
includeLinks = false,
|
|
458
461
|
includeMeta = false,
|
|
459
462
|
includeIncluded = false,
|
|
463
|
+
allowSparseFields = false,
|
|
460
464
|
excludeAttributeKeys = [],
|
|
461
465
|
relationshipEntries = []
|
|
462
466
|
} = {}) {
|
|
@@ -475,6 +479,7 @@ function createJsonApiResourceSuccessTransportSchema({
|
|
|
475
479
|
type,
|
|
476
480
|
attributes,
|
|
477
481
|
requireId: true,
|
|
482
|
+
allowSparseFields,
|
|
478
483
|
excludeAttributeKeys,
|
|
479
484
|
relationshipEntries
|
|
480
485
|
});
|
|
@@ -890,6 +895,7 @@ function createJsonApiResourceRouteContract({
|
|
|
890
895
|
outputKind = "record",
|
|
891
896
|
successStatus = 200,
|
|
892
897
|
includeValidation400 = false,
|
|
898
|
+
allowSparseFields = false,
|
|
893
899
|
allowBodyId = false,
|
|
894
900
|
pointerPrefix = "/data/attributes",
|
|
895
901
|
bodyAttributeExcludeKeys = [],
|
|
@@ -984,6 +990,7 @@ function createJsonApiResourceRouteContract({
|
|
|
984
990
|
includeLinks: typeof getDocumentLinks === "function",
|
|
985
991
|
includeMeta: typeof getDocumentMeta === "function" || normalizedOutputKind === "collection",
|
|
986
992
|
includeIncluded: typeof getIncluded === "function",
|
|
993
|
+
allowSparseFields,
|
|
987
994
|
excludeAttributeKeys: outputAttributeExcludeKeys,
|
|
988
995
|
relationshipEntries: outputRelationshipEntries
|
|
989
996
|
})
|
|
@@ -3,6 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
JSON_API_CONTENT_TYPE,
|
|
6
|
+
encodeJsonApiResourceQueryObject,
|
|
6
7
|
createJsonApiResourceQueryTransportSchema,
|
|
7
8
|
createJsonApiResourceRequestBodyTransportSchema,
|
|
8
9
|
createJsonApiResourceRouteContract,
|
|
@@ -70,6 +71,17 @@ const CONTACT_LIST_QUERY_SCHEMA = Object.freeze({
|
|
|
70
71
|
type: "string",
|
|
71
72
|
required: false
|
|
72
73
|
},
|
|
74
|
+
fields: {
|
|
75
|
+
type: "object",
|
|
76
|
+
required: false,
|
|
77
|
+
values: {
|
|
78
|
+
type: "array",
|
|
79
|
+
items: {
|
|
80
|
+
type: "string",
|
|
81
|
+
minLength: 1
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
73
85
|
workspaceId: {
|
|
74
86
|
type: "string",
|
|
75
87
|
required: false,
|
|
@@ -198,6 +210,23 @@ test("createJsonApiResourceQueryTransportSchema and route transport map list que
|
|
|
198
210
|
assert.ok(Object.hasOwn(schema.properties, "filter[q]"));
|
|
199
211
|
assert.ok(Object.hasOwn(schema.properties, "include"));
|
|
200
212
|
assert.ok(Object.hasOwn(schema.properties, "filter[workspaceId]"));
|
|
213
|
+
assert.ok(Object.hasOwn(schema.properties, "fields"));
|
|
214
|
+
assert.ok(Object.hasOwn(schema.patternProperties, "^fields\\[[^\\[\\]]+\\]$"));
|
|
215
|
+
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
encodeJsonApiResourceQueryObject({
|
|
218
|
+
fields: {
|
|
219
|
+
contacts: ["id", "name"],
|
|
220
|
+
workspaces: ["id", "slug"]
|
|
221
|
+
}
|
|
222
|
+
}, {
|
|
223
|
+
responseType: "contacts"
|
|
224
|
+
}),
|
|
225
|
+
{
|
|
226
|
+
"fields[contacts]": "id,name",
|
|
227
|
+
"fields[workspaces]": "id,slug"
|
|
228
|
+
}
|
|
229
|
+
);
|
|
201
230
|
|
|
202
231
|
const transport = createJsonApiResourceRouteTransport({
|
|
203
232
|
type: "contacts",
|
|
@@ -210,7 +239,9 @@ test("createJsonApiResourceQueryTransportSchema and route transport map list que
|
|
|
210
239
|
"page[limit]": "10",
|
|
211
240
|
"filter[q]": "Merc",
|
|
212
241
|
include: "workspace",
|
|
213
|
-
"filter[workspaceId]": "7"
|
|
242
|
+
"filter[workspaceId]": "7",
|
|
243
|
+
"fields[contacts]": "id,name",
|
|
244
|
+
"fields[workspaces]": "id,slug"
|
|
214
245
|
});
|
|
215
246
|
|
|
216
247
|
assert.deepEqual(plainQuery, {
|
|
@@ -218,8 +249,41 @@ test("createJsonApiResourceQueryTransportSchema and route transport map list que
|
|
|
218
249
|
limit: "10",
|
|
219
250
|
q: "Merc",
|
|
220
251
|
include: "workspace",
|
|
221
|
-
workspaceId: "7"
|
|
252
|
+
workspaceId: "7",
|
|
253
|
+
fields: {
|
|
254
|
+
contacts: ["id", "name"],
|
|
255
|
+
workspaces: ["id", "slug"]
|
|
256
|
+
}
|
|
222
257
|
});
|
|
258
|
+
|
|
259
|
+
assert.deepEqual(
|
|
260
|
+
transport.request.query({
|
|
261
|
+
fields: "name,id"
|
|
262
|
+
}),
|
|
263
|
+
{
|
|
264
|
+
fields: {
|
|
265
|
+
contacts: ["id", "name"]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("sparse JSON:API response contracts preserve field types without requiring every attribute", () => {
|
|
272
|
+
const schema = createJsonApiResourceSuccessTransportSchema({
|
|
273
|
+
type: "contacts",
|
|
274
|
+
attributes: CONTACT_RECORD_SCHEMA,
|
|
275
|
+
kind: "collection",
|
|
276
|
+
allowSparseFields: true
|
|
277
|
+
});
|
|
278
|
+
const resourceSchema = schema.definitions.contactsSuccessResource;
|
|
279
|
+
const attributesSchemaName = resourceSchema.properties.attributes.allOf[0].$ref
|
|
280
|
+
.replace("#/definitions/", "");
|
|
281
|
+
const attributesSchema = schema.definitions[attributesSchemaName];
|
|
282
|
+
|
|
283
|
+
assert.deepEqual(resourceSchema.required, ["type", "id"]);
|
|
284
|
+
assert.equal(Object.hasOwn(attributesSchema, "required"), false);
|
|
285
|
+
assert.equal(attributesSchema.additionalProperties, false);
|
|
286
|
+
assert.equal(attributesSchema.properties.subscribed.anyOf[0].type, "boolean");
|
|
223
287
|
});
|
|
224
288
|
|
|
225
289
|
test("createJsonApiResourceRouteContract produces route options compatible with kernel route validator", () => {
|