@jskit-ai/http-runtime 0.1.56 → 0.1.57
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.57",
|
|
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.58"
|
|
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.57",
|
|
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.58",
|
|
22
22
|
"json-rest-schema": "1.x.x"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -36,7 +36,21 @@ function normalizeJsonApiClientTransport(transport = null) {
|
|
|
36
36
|
responseKind: normalizeText(transport?.responseKind, {
|
|
37
37
|
fallback: "record"
|
|
38
38
|
}).toLowerCase(),
|
|
39
|
-
includeBodyId: transport?.includeBodyId === true
|
|
39
|
+
includeBodyId: transport?.includeBodyId === true,
|
|
40
|
+
lookupContainerKey: normalizeText(transport?.lookupContainerKey, {
|
|
41
|
+
fallback: "lookups"
|
|
42
|
+
}),
|
|
43
|
+
lookupFieldMap: Object.freeze(
|
|
44
|
+
Object.fromEntries(
|
|
45
|
+
Object.entries(normalizeObject(transport?.lookupFieldMap))
|
|
46
|
+
.map(([relationshipName, fieldKey]) => [
|
|
47
|
+
normalizeText(relationshipName),
|
|
48
|
+
normalizeText(fieldKey)
|
|
49
|
+
])
|
|
50
|
+
.filter(([relationshipName, fieldKey]) => relationshipName && fieldKey)
|
|
51
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
52
|
+
)
|
|
53
|
+
)
|
|
40
54
|
});
|
|
41
55
|
}
|
|
42
56
|
|
|
@@ -84,12 +98,119 @@ function encodeJsonApiResourceQuery(query, transport = null) {
|
|
|
84
98
|
});
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
function
|
|
101
|
+
function resolveRelationshipFieldKey(relationshipName = "", lookupFieldMap = null) {
|
|
102
|
+
const normalizedRelationshipName = normalizeText(relationshipName);
|
|
103
|
+
const explicitFieldKey = normalizeText(lookupFieldMap?.[normalizedRelationshipName]);
|
|
104
|
+
if (explicitFieldKey) {
|
|
105
|
+
return explicitFieldKey;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalizedRelationshipName ? `${normalizedRelationshipName}Id` : "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createIncludedResourceIndex(document = {}) {
|
|
112
|
+
const index = new Map();
|
|
113
|
+
|
|
114
|
+
for (const resource of normalizeArray(document?.included)) {
|
|
115
|
+
const type = normalizeText(resource?.type);
|
|
116
|
+
const id = normalizeText(resource?.id);
|
|
117
|
+
if (!type || !id) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
index.set(`${type}:${id}`, resource);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function simplifyRelationshipResource(linkage = {}, options = {}) {
|
|
128
|
+
const normalizedLinkage = isRecord(linkage) ? normalizeObject(linkage) : {};
|
|
129
|
+
const type = normalizeText(normalizedLinkage.type);
|
|
130
|
+
const id = normalizeText(normalizedLinkage.id);
|
|
131
|
+
if (!type || !id) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const resourceKey = `${type}:${id}`;
|
|
136
|
+
const includedResource = options.includedResourceIndex?.get(resourceKey);
|
|
137
|
+
if (!includedResource) {
|
|
138
|
+
return {
|
|
139
|
+
id
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return simplifyResourceObject(includedResource, options);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function simplifyResourceObject(resource = {}, options = {}) {
|
|
88
147
|
const normalizedResource = isRecord(resource) ? normalizeObject(resource) : {};
|
|
89
|
-
|
|
148
|
+
const simplified = {
|
|
90
149
|
id: normalizedResource.id == null ? "" : String(normalizedResource.id),
|
|
91
150
|
...(normalizeObject(normalizedResource.attributes))
|
|
92
151
|
};
|
|
152
|
+
const lookupContainerKey = normalizeText(options.lookupContainerKey, {
|
|
153
|
+
fallback: "lookups"
|
|
154
|
+
});
|
|
155
|
+
const lookupFieldMap = options.lookupFieldMap || null;
|
|
156
|
+
const resourceKey = `${normalizeText(normalizedResource.type)}:${normalizeText(normalizedResource.id)}`;
|
|
157
|
+
if (resourceKey && options.inFlightKeys?.has(resourceKey)) {
|
|
158
|
+
return simplified;
|
|
159
|
+
}
|
|
160
|
+
if (resourceKey && options.resourceCache?.has(resourceKey)) {
|
|
161
|
+
return options.resourceCache.get(resourceKey);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (resourceKey) {
|
|
165
|
+
options.inFlightKeys?.add(resourceKey);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const lookups = {};
|
|
169
|
+
for (const [relationshipName, relationshipValue] of Object.entries(normalizeObject(normalizedResource.relationships))) {
|
|
170
|
+
const relationshipData = relationshipValue?.data;
|
|
171
|
+
if (Array.isArray(relationshipData)) {
|
|
172
|
+
const items = relationshipData
|
|
173
|
+
.map((entry) =>
|
|
174
|
+
simplifyRelationshipResource(entry, {
|
|
175
|
+
...options,
|
|
176
|
+
lookupFieldMap: null
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
if (items.length > 0) {
|
|
181
|
+
lookups[relationshipName] = items;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fieldKey = resolveRelationshipFieldKey(relationshipName, lookupFieldMap);
|
|
187
|
+
if (!Object.hasOwn(simplified, fieldKey)) {
|
|
188
|
+
simplified[fieldKey] = relationshipData?.id == null ? null : String(relationshipData.id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lookupRecord = relationshipData == null
|
|
192
|
+
? null
|
|
193
|
+
: simplifyRelationshipResource(relationshipData, {
|
|
194
|
+
...options,
|
|
195
|
+
lookupFieldMap: null
|
|
196
|
+
});
|
|
197
|
+
if (lookupRecord) {
|
|
198
|
+
lookups[fieldKey] = lookupRecord;
|
|
199
|
+
if (relationshipName !== fieldKey) {
|
|
200
|
+
lookups[relationshipName] = lookupRecord;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (Object.keys(lookups).length > 0) {
|
|
206
|
+
simplified[lookupContainerKey] = lookups;
|
|
207
|
+
}
|
|
208
|
+
if (resourceKey) {
|
|
209
|
+
options.inFlightKeys?.delete(resourceKey);
|
|
210
|
+
options.resourceCache?.set(resourceKey, simplified);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return simplified;
|
|
93
214
|
}
|
|
94
215
|
|
|
95
216
|
function assertPrimaryDataType(resource = {}, expectedType = "") {
|
|
@@ -122,6 +243,13 @@ function decodeJsonApiResourceResponse(payload, transport = null) {
|
|
|
122
243
|
}
|
|
123
244
|
|
|
124
245
|
const document = normalizeJsonApiDocument(payload);
|
|
246
|
+
const simplifyOptions = {
|
|
247
|
+
lookupContainerKey: normalizedTransport.lookupContainerKey,
|
|
248
|
+
lookupFieldMap: normalizedTransport.lookupFieldMap,
|
|
249
|
+
includedResourceIndex: createIncludedResourceIndex(document),
|
|
250
|
+
resourceCache: new Map(),
|
|
251
|
+
inFlightKeys: new Set()
|
|
252
|
+
};
|
|
125
253
|
if (document.kind === "unknown") {
|
|
126
254
|
throw new Error("Expected JSON:API response document.");
|
|
127
255
|
}
|
|
@@ -136,7 +264,7 @@ function decodeJsonApiResourceResponse(payload, transport = null) {
|
|
|
136
264
|
}
|
|
137
265
|
|
|
138
266
|
return {
|
|
139
|
-
items: document.data.map((entry) => simplifyResourceObject(entry)),
|
|
267
|
+
items: document.data.map((entry) => simplifyResourceObject(entry, simplifyOptions)),
|
|
140
268
|
...resolveCollectionPageMeta(document)
|
|
141
269
|
};
|
|
142
270
|
}
|
|
@@ -158,7 +286,7 @@ function decodeJsonApiResourceResponse(payload, transport = null) {
|
|
|
158
286
|
assertPrimaryDataType(document.data, normalizedTransport.responseType);
|
|
159
287
|
}
|
|
160
288
|
|
|
161
|
-
return document.data == null ? null : simplifyResourceObject(document.data);
|
|
289
|
+
return document.data == null ? null : simplifyResourceObject(document.data, simplifyOptions);
|
|
162
290
|
}
|
|
163
291
|
|
|
164
292
|
if (document.kind !== "resource") {
|
|
@@ -169,7 +297,7 @@ function decodeJsonApiResourceResponse(payload, transport = null) {
|
|
|
169
297
|
assertPrimaryDataType(document.data, normalizedTransport.responseType);
|
|
170
298
|
}
|
|
171
299
|
|
|
172
|
-
return document.data == null ? null : simplifyResourceObject(document.data);
|
|
300
|
+
return document.data == null ? null : simplifyResourceObject(document.data, simplifyOptions);
|
|
173
301
|
}
|
|
174
302
|
|
|
175
303
|
function decodeJsonApiErrorFieldErrors(payload = {}) {
|
|
@@ -54,6 +54,54 @@ const JSON_API_META_SCHEMA = Object.freeze({
|
|
|
54
54
|
additionalProperties: true
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
const JSON_API_RESOURCE_IDENTIFIER_SCHEMA = Object.freeze({
|
|
58
|
+
type: "object",
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
required: ["type", "id"],
|
|
61
|
+
properties: {
|
|
62
|
+
type: { type: "string", minLength: 1 },
|
|
63
|
+
id: JSON_API_ID_SCHEMA
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const JSON_API_RELATIONSHIP_OBJECT_SCHEMA = Object.freeze({
|
|
68
|
+
type: "object",
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
required: ["data"],
|
|
71
|
+
properties: {
|
|
72
|
+
data: {
|
|
73
|
+
anyOf: [
|
|
74
|
+
JSON_API_RESOURCE_IDENTIFIER_SCHEMA,
|
|
75
|
+
{
|
|
76
|
+
type: "array",
|
|
77
|
+
items: JSON_API_RESOURCE_IDENTIFIER_SCHEMA
|
|
78
|
+
},
|
|
79
|
+
{ type: "null" }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const JSON_API_INCLUDED_RESOURCE_SCHEMA = Object.freeze({
|
|
86
|
+
type: "object",
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
required: ["type", "id"],
|
|
89
|
+
properties: {
|
|
90
|
+
type: { type: "string", minLength: 1 },
|
|
91
|
+
id: JSON_API_ID_SCHEMA,
|
|
92
|
+
attributes: {
|
|
93
|
+
type: "object",
|
|
94
|
+
additionalProperties: true
|
|
95
|
+
},
|
|
96
|
+
relationships: {
|
|
97
|
+
type: "object",
|
|
98
|
+
additionalProperties: JSON_API_RELATIONSHIP_OBJECT_SCHEMA
|
|
99
|
+
},
|
|
100
|
+
links: JSON_API_LINKS_SCHEMA,
|
|
101
|
+
meta: JSON_API_META_SCHEMA
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
57
105
|
const JSON_API_ERROR_OBJECT_SCHEMA = Object.freeze({
|
|
58
106
|
type: "object",
|
|
59
107
|
additionalProperties: false,
|
|
@@ -120,7 +168,8 @@ function resolveRouteTypes(value = {}) {
|
|
|
120
168
|
function resolveEmbeddedAttributesTransportSchema(definition, {
|
|
121
169
|
context = "JSON:API resource",
|
|
122
170
|
defaultMode = "replace",
|
|
123
|
-
removeId = false
|
|
171
|
+
removeId = false,
|
|
172
|
+
removeKeys = []
|
|
124
173
|
} = {}) {
|
|
125
174
|
const transportSchema = resolveSchemaTransportSchemaDefinition(definition, {
|
|
126
175
|
context,
|
|
@@ -136,13 +185,35 @@ function resolveEmbeddedAttributesTransportSchema(definition, {
|
|
|
136
185
|
const nextProperties = {
|
|
137
186
|
...properties
|
|
138
187
|
};
|
|
188
|
+
const excludedKeys = new Set(
|
|
189
|
+
normalizeArray(removeKeys)
|
|
190
|
+
.map((entry) => String(entry || "").trim())
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
);
|
|
139
193
|
|
|
140
194
|
if (removeId) {
|
|
141
|
-
|
|
195
|
+
excludedKeys.add("id");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const key of excludedKeys) {
|
|
199
|
+
delete nextProperties[key];
|
|
142
200
|
}
|
|
143
201
|
|
|
202
|
+
const schemaFieldDefinitions = (() => {
|
|
203
|
+
const fieldDefinitions = definition?.schema?.getFieldDefinitions?.();
|
|
204
|
+
return isRecord(fieldDefinitions) ? fieldDefinitions : {};
|
|
205
|
+
})();
|
|
206
|
+
|
|
144
207
|
const required = Array.isArray(sourceSchema.required)
|
|
145
|
-
? sourceSchema.required.filter((entry) =>
|
|
208
|
+
? sourceSchema.required.filter((entry) => {
|
|
209
|
+
const normalizedEntry = String(entry || "").trim();
|
|
210
|
+
if (!normalizedEntry || excludedKeys.has(normalizedEntry)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fieldDefinition = normalizeObject(schemaFieldDefinitions[normalizedEntry]);
|
|
215
|
+
return fieldDefinition.nullable !== true;
|
|
216
|
+
})
|
|
146
217
|
: [];
|
|
147
218
|
|
|
148
219
|
const attributeSchema = {
|
|
@@ -160,18 +231,124 @@ function resolveEmbeddedAttributesTransportSchema(definition, {
|
|
|
160
231
|
return createEmbeddableTransportSchemaDocument(attributeSchema, `${normalizeText(context, { fallback: "JsonApiAttributes" }).replace(/[^a-z0-9]+/gi, "_")}`);
|
|
161
232
|
}
|
|
162
233
|
|
|
234
|
+
function normalizeRelationshipSchemaEntries(entries = []) {
|
|
235
|
+
const normalizedEntries = [];
|
|
236
|
+
|
|
237
|
+
for (const entry of normalizeArray(entries)) {
|
|
238
|
+
const source = normalizeObject(entry);
|
|
239
|
+
const relationshipName = normalizeText(source.relationshipName || source.name);
|
|
240
|
+
const relationshipType = normalizeText(source.relationshipType || source.type);
|
|
241
|
+
const attributeKey = normalizeText(source.attributeKey || source.fieldKey);
|
|
242
|
+
if (!relationshipName) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
normalizedEntries.push(Object.freeze({
|
|
247
|
+
relationshipName,
|
|
248
|
+
relationshipType,
|
|
249
|
+
attributeKey,
|
|
250
|
+
required: source.required === true,
|
|
251
|
+
nullable: source.nullable === true
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return Object.freeze(normalizedEntries);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createJsonApiRelationshipDataSchema(relationshipType = "", {
|
|
259
|
+
nullable = false
|
|
260
|
+
} = {}) {
|
|
261
|
+
const normalizedRelationshipType = normalizeText(relationshipType);
|
|
262
|
+
const resourceIdentifierSchema = normalizedRelationshipType
|
|
263
|
+
? {
|
|
264
|
+
type: "object",
|
|
265
|
+
additionalProperties: false,
|
|
266
|
+
required: ["type", "id"],
|
|
267
|
+
properties: {
|
|
268
|
+
type: { const: normalizedRelationshipType },
|
|
269
|
+
id: JSON_API_ID_SCHEMA
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
: JSON_API_RESOURCE_IDENTIFIER_SCHEMA;
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
anyOf: nullable
|
|
276
|
+
? [
|
|
277
|
+
resourceIdentifierSchema,
|
|
278
|
+
{ type: "null" }
|
|
279
|
+
]
|
|
280
|
+
: [resourceIdentifierSchema]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function createJsonApiRelationshipsTransportSchema(entries = [], {
|
|
285
|
+
includeRequired = false
|
|
286
|
+
} = {}) {
|
|
287
|
+
const relationshipEntries = normalizeRelationshipSchemaEntries(entries);
|
|
288
|
+
if (relationshipEntries.length < 1) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const properties = {};
|
|
293
|
+
const required = [];
|
|
294
|
+
|
|
295
|
+
for (const entry of relationshipEntries) {
|
|
296
|
+
properties[entry.relationshipName] = {
|
|
297
|
+
type: "object",
|
|
298
|
+
additionalProperties: false,
|
|
299
|
+
required: ["data"],
|
|
300
|
+
properties: {
|
|
301
|
+
data: createJsonApiRelationshipDataSchema(entry.relationshipType, {
|
|
302
|
+
nullable: entry.nullable
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (includeRequired && entry.required) {
|
|
308
|
+
required.push(entry.relationshipName);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const schema = {
|
|
313
|
+
type: "object",
|
|
314
|
+
additionalProperties: false,
|
|
315
|
+
properties
|
|
316
|
+
};
|
|
317
|
+
if (required.length > 0) {
|
|
318
|
+
schema.required = required;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return Object.freeze({
|
|
322
|
+
schema,
|
|
323
|
+
entries: relationshipEntries
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
163
327
|
function createJsonApiResourceObjectTransportSchema({
|
|
164
328
|
type = "",
|
|
165
329
|
attributes,
|
|
166
330
|
requireId = true,
|
|
167
331
|
includeLinks = false,
|
|
168
|
-
includeMeta = false
|
|
332
|
+
includeMeta = false,
|
|
333
|
+
excludeAttributeKeys = [],
|
|
334
|
+
relationshipEntries = [],
|
|
335
|
+
relationshipMembersRequired = false
|
|
169
336
|
} = {}) {
|
|
170
337
|
const normalizedType = resolveRouteType(type);
|
|
338
|
+
const relationshipsTransport = createJsonApiRelationshipsTransportSchema(relationshipEntries, {
|
|
339
|
+
includeRequired: relationshipMembersRequired
|
|
340
|
+
});
|
|
341
|
+
const relationFieldKeys = relationshipsTransport
|
|
342
|
+
? relationshipsTransport.entries.map((entry) => entry.attributeKey).filter(Boolean)
|
|
343
|
+
: [];
|
|
171
344
|
const embeddedAttributes = resolveEmbeddedAttributesTransportSchema(attributes, {
|
|
172
345
|
context: `${normalizedType} resource attributes`,
|
|
173
346
|
defaultMode: "replace",
|
|
174
|
-
removeId: true
|
|
347
|
+
removeId: true,
|
|
348
|
+
removeKeys: [
|
|
349
|
+
...normalizeArray(excludeAttributeKeys),
|
|
350
|
+
...relationFieldKeys,
|
|
351
|
+
]
|
|
175
352
|
});
|
|
176
353
|
|
|
177
354
|
const properties = {
|
|
@@ -187,6 +364,10 @@ function createJsonApiResourceObjectTransportSchema({
|
|
|
187
364
|
required.push("id");
|
|
188
365
|
}
|
|
189
366
|
|
|
367
|
+
if (relationshipsTransport) {
|
|
368
|
+
properties.relationships = relationshipsTransport.schema;
|
|
369
|
+
}
|
|
370
|
+
|
|
190
371
|
if (includeLinks) {
|
|
191
372
|
properties.links = JSON_API_LINKS_SCHEMA;
|
|
192
373
|
}
|
|
@@ -201,19 +382,25 @@ function createJsonApiResourceObjectTransportSchema({
|
|
|
201
382
|
required,
|
|
202
383
|
properties
|
|
203
384
|
},
|
|
204
|
-
definitions: embeddedAttributes.definitions
|
|
385
|
+
definitions: embeddedAttributes.definitions,
|
|
386
|
+
hasRelationships: Boolean(relationshipsTransport)
|
|
205
387
|
};
|
|
206
388
|
}
|
|
207
389
|
|
|
208
390
|
function createJsonApiResourceRequestBodyTransportSchema({
|
|
209
391
|
type = "",
|
|
210
392
|
attributes,
|
|
211
|
-
requireId = false
|
|
393
|
+
requireId = false,
|
|
394
|
+
excludeAttributeKeys = [],
|
|
395
|
+
relationshipEntries = []
|
|
212
396
|
} = {}) {
|
|
213
397
|
const resourceTransport = createJsonApiResourceObjectTransportSchema({
|
|
214
398
|
type,
|
|
215
399
|
attributes,
|
|
216
|
-
requireId
|
|
400
|
+
requireId,
|
|
401
|
+
excludeAttributeKeys,
|
|
402
|
+
relationshipEntries,
|
|
403
|
+
relationshipMembersRequired: true
|
|
217
404
|
});
|
|
218
405
|
const embeddedResource = createEmbeddableTransportSchemaDocument(
|
|
219
406
|
{
|
|
@@ -260,7 +447,9 @@ function createJsonApiResourceSuccessTransportSchema({
|
|
|
260
447
|
kind = "record",
|
|
261
448
|
includeLinks = false,
|
|
262
449
|
includeMeta = false,
|
|
263
|
-
includeIncluded = false
|
|
450
|
+
includeIncluded = false,
|
|
451
|
+
excludeAttributeKeys = [],
|
|
452
|
+
relationshipEntries = []
|
|
264
453
|
} = {}) {
|
|
265
454
|
const normalizedKind = String(kind || "record").trim().toLowerCase();
|
|
266
455
|
if (!["record", "nullable-record", "collection", "meta"].includes(normalizedKind)) {
|
|
@@ -276,7 +465,9 @@ function createJsonApiResourceSuccessTransportSchema({
|
|
|
276
465
|
const resourceTransport = createJsonApiResourceObjectTransportSchema({
|
|
277
466
|
type,
|
|
278
467
|
attributes,
|
|
279
|
-
requireId: true
|
|
468
|
+
requireId: true,
|
|
469
|
+
excludeAttributeKeys,
|
|
470
|
+
relationshipEntries
|
|
280
471
|
});
|
|
281
472
|
const embeddedResource = createEmbeddableTransportSchemaDocument(
|
|
282
473
|
{
|
|
@@ -308,10 +499,10 @@ function createJsonApiResourceSuccessTransportSchema({
|
|
|
308
499
|
documentProperties.data = embeddedResource.schema;
|
|
309
500
|
}
|
|
310
501
|
|
|
311
|
-
if (includeIncluded) {
|
|
502
|
+
if (includeIncluded || resourceTransport.hasRelationships) {
|
|
312
503
|
documentProperties.included = {
|
|
313
504
|
type: "array",
|
|
314
|
-
items:
|
|
505
|
+
items: JSON_API_INCLUDED_RESOURCE_SCHEMA
|
|
315
506
|
};
|
|
316
507
|
}
|
|
317
508
|
if (includeLinks) {
|
|
@@ -692,6 +883,10 @@ function createJsonApiResourceRouteContract({
|
|
|
692
883
|
includeValidation400 = false,
|
|
693
884
|
allowBodyId = false,
|
|
694
885
|
pointerPrefix = "/data/attributes",
|
|
886
|
+
bodyAttributeExcludeKeys = [],
|
|
887
|
+
outputAttributeExcludeKeys = [],
|
|
888
|
+
bodyRelationshipEntries = [],
|
|
889
|
+
outputRelationshipEntries = [],
|
|
695
890
|
getRecordType = null,
|
|
696
891
|
getRecordId = null,
|
|
697
892
|
getRecordAttributes = null,
|
|
@@ -747,7 +942,9 @@ function createJsonApiResourceRouteContract({
|
|
|
747
942
|
fastifySchema.body = createJsonApiResourceRequestBodyTransportSchema({
|
|
748
943
|
type: normalizedRequestType,
|
|
749
944
|
attributes: body,
|
|
750
|
-
requireId: allowBodyId
|
|
945
|
+
requireId: allowBodyId,
|
|
946
|
+
excludeAttributeKeys: bodyAttributeExcludeKeys,
|
|
947
|
+
relationshipEntries: bodyRelationshipEntries
|
|
751
948
|
});
|
|
752
949
|
}
|
|
753
950
|
if (query) {
|
|
@@ -777,7 +974,9 @@ function createJsonApiResourceRouteContract({
|
|
|
777
974
|
kind: normalizedOutputKind,
|
|
778
975
|
includeLinks: typeof getDocumentLinks === "function",
|
|
779
976
|
includeMeta: typeof getDocumentMeta === "function" || normalizedOutputKind === "collection",
|
|
780
|
-
includeIncluded: typeof getIncluded === "function"
|
|
977
|
+
includeIncluded: typeof getIncluded === "function",
|
|
978
|
+
excludeAttributeKeys: outputAttributeExcludeKeys,
|
|
979
|
+
relationshipEntries: outputRelationshipEntries
|
|
781
980
|
})
|
|
782
981
|
);
|
|
783
982
|
}
|
package/test/client.test.js
CHANGED
|
@@ -148,6 +148,173 @@ test("request encodes and decodes json:api resource transport for records", asyn
|
|
|
148
148
|
);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test("request decodes json:api relationship includes into JSKIT lookups and foreign-key fields", async () => {
|
|
152
|
+
const fetchImpl = async () =>
|
|
153
|
+
mockResponse({
|
|
154
|
+
contentType: "application/vnd.api+json",
|
|
155
|
+
data: {
|
|
156
|
+
data: {
|
|
157
|
+
type: "pets",
|
|
158
|
+
id: "729900",
|
|
159
|
+
attributes: {
|
|
160
|
+
name: "Daisy"
|
|
161
|
+
},
|
|
162
|
+
relationships: {
|
|
163
|
+
contact: {
|
|
164
|
+
data: {
|
|
165
|
+
type: "contacts",
|
|
166
|
+
id: "552252"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
breed: {
|
|
170
|
+
data: {
|
|
171
|
+
type: "breeds",
|
|
172
|
+
id: "2"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
included: [
|
|
178
|
+
{
|
|
179
|
+
type: "contacts",
|
|
180
|
+
id: "552252",
|
|
181
|
+
attributes: {
|
|
182
|
+
firstName: "Serena",
|
|
183
|
+
lastName: "Ellison"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
type: "breeds",
|
|
188
|
+
id: "2",
|
|
189
|
+
attributes: {
|
|
190
|
+
name: "Cavoodle"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const client = createHttpClient({ fetchImpl });
|
|
198
|
+
const payload = await client.request("/api/pets/729900", {
|
|
199
|
+
method: "GET",
|
|
200
|
+
transport: {
|
|
201
|
+
kind: "jsonapi-resource",
|
|
202
|
+
responseType: "pets",
|
|
203
|
+
responseKind: "record",
|
|
204
|
+
lookupFieldMap: {
|
|
205
|
+
contact: "contactId",
|
|
206
|
+
breed: "breedId"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.deepEqual(payload, {
|
|
212
|
+
id: "729900",
|
|
213
|
+
name: "Daisy",
|
|
214
|
+
contactId: "552252",
|
|
215
|
+
breedId: "2",
|
|
216
|
+
lookups: {
|
|
217
|
+
contactId: {
|
|
218
|
+
id: "552252",
|
|
219
|
+
firstName: "Serena",
|
|
220
|
+
lastName: "Ellison"
|
|
221
|
+
},
|
|
222
|
+
contact: {
|
|
223
|
+
id: "552252",
|
|
224
|
+
firstName: "Serena",
|
|
225
|
+
lastName: "Ellison"
|
|
226
|
+
},
|
|
227
|
+
breedId: {
|
|
228
|
+
id: "2",
|
|
229
|
+
name: "Cavoodle"
|
|
230
|
+
},
|
|
231
|
+
breed: {
|
|
232
|
+
id: "2",
|
|
233
|
+
name: "Cavoodle"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("request recursively decodes nested included relationships for collection-style lookups", async () => {
|
|
240
|
+
const fetchImpl = async () =>
|
|
241
|
+
mockResponse({
|
|
242
|
+
contentType: "application/vnd.api+json",
|
|
243
|
+
data: {
|
|
244
|
+
data: {
|
|
245
|
+
type: "contacts",
|
|
246
|
+
id: "552252",
|
|
247
|
+
attributes: {
|
|
248
|
+
firstName: "Serena",
|
|
249
|
+
lastName: "Ellison"
|
|
250
|
+
},
|
|
251
|
+
relationships: {
|
|
252
|
+
pets: {
|
|
253
|
+
data: [
|
|
254
|
+
{
|
|
255
|
+
type: "pets",
|
|
256
|
+
id: "729900"
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
included: [
|
|
263
|
+
{
|
|
264
|
+
type: "pets",
|
|
265
|
+
id: "729900",
|
|
266
|
+
attributes: {
|
|
267
|
+
name: "Daisy"
|
|
268
|
+
},
|
|
269
|
+
relationships: {
|
|
270
|
+
breed: {
|
|
271
|
+
data: {
|
|
272
|
+
type: "breeds",
|
|
273
|
+
id: "2"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: "breeds",
|
|
280
|
+
id: "2",
|
|
281
|
+
attributes: {
|
|
282
|
+
name: "Cavoodle"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const client = createHttpClient({ fetchImpl });
|
|
290
|
+
const payload = await client.request("/api/contacts/552252", {
|
|
291
|
+
method: "GET",
|
|
292
|
+
transport: {
|
|
293
|
+
kind: "jsonapi-resource",
|
|
294
|
+
responseType: "contacts",
|
|
295
|
+
responseKind: "record"
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
assert.deepEqual(payload.lookups?.pets, [
|
|
300
|
+
{
|
|
301
|
+
id: "729900",
|
|
302
|
+
name: "Daisy",
|
|
303
|
+
breedId: "2",
|
|
304
|
+
lookups: {
|
|
305
|
+
breedId: {
|
|
306
|
+
id: "2",
|
|
307
|
+
name: "Cavoodle"
|
|
308
|
+
},
|
|
309
|
+
breed: {
|
|
310
|
+
id: "2",
|
|
311
|
+
name: "Cavoodle"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
]);
|
|
316
|
+
});
|
|
317
|
+
|
|
151
318
|
test("request decodes json:api collection responses into JSKIT paged-list shape", async () => {
|
|
152
319
|
const fetchImpl = async () =>
|
|
153
320
|
mockResponse({
|
|
@@ -302,6 +302,76 @@ test("createJsonApiResourceRouteTransport passes through tagged JSON:API documen
|
|
|
302
302
|
assert.deepEqual(transport.response(returnJsonApiDocument(document)), document);
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
+
test("createJsonApiResourceRouteContract models explicit relationship-backed output fields as relationships", () => {
|
|
306
|
+
const contract = createJsonApiResourceRouteContract({
|
|
307
|
+
responseType: "availabilities",
|
|
308
|
+
output: {
|
|
309
|
+
schema: createSchema({
|
|
310
|
+
id: {
|
|
311
|
+
type: "string",
|
|
312
|
+
required: true,
|
|
313
|
+
minLength: 1
|
|
314
|
+
},
|
|
315
|
+
serviceId: {
|
|
316
|
+
type: "string",
|
|
317
|
+
required: true,
|
|
318
|
+
nullable: true,
|
|
319
|
+
belongsTo: "services",
|
|
320
|
+
as: "service"
|
|
321
|
+
},
|
|
322
|
+
name: {
|
|
323
|
+
type: "string",
|
|
324
|
+
required: true,
|
|
325
|
+
minLength: 1
|
|
326
|
+
},
|
|
327
|
+
deletedAt: {
|
|
328
|
+
type: "string",
|
|
329
|
+
required: true,
|
|
330
|
+
nullable: true
|
|
331
|
+
},
|
|
332
|
+
lookups: {
|
|
333
|
+
type: "object",
|
|
334
|
+
required: false
|
|
335
|
+
}
|
|
336
|
+
}),
|
|
337
|
+
mode: "replace"
|
|
338
|
+
},
|
|
339
|
+
outputKind: "record",
|
|
340
|
+
outputAttributeExcludeKeys: ["lookups"],
|
|
341
|
+
outputRelationshipEntries: [
|
|
342
|
+
{
|
|
343
|
+
attributeKey: "serviceId",
|
|
344
|
+
relationshipName: "service",
|
|
345
|
+
relationshipType: "services",
|
|
346
|
+
nullable: true
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const resourceSchema = contract.responses["200"].transportSchema.definitions.availabilitiesSuccessResource;
|
|
352
|
+
const attributesSchemaRef = resourceSchema.properties.attributes.allOf[0].$ref;
|
|
353
|
+
const attributesSchemaName = attributesSchemaRef.replace("#/definitions/", "");
|
|
354
|
+
const attributesSchema = contract.responses["200"].transportSchema.definitions[attributesSchemaName];
|
|
355
|
+
|
|
356
|
+
assert.deepEqual(resourceSchema.required, ["type", "attributes", "id"]);
|
|
357
|
+
assert.ok(Object.hasOwn(resourceSchema.properties, "relationships"));
|
|
358
|
+
assert.equal(attributesSchema.required.includes("serviceId"), false);
|
|
359
|
+
assert.equal(attributesSchema.required.includes("deletedAt"), false);
|
|
360
|
+
assert.equal(attributesSchema.required.includes("lookups"), false);
|
|
361
|
+
assert.equal(Object.hasOwn(attributesSchema.properties, "serviceId"), false);
|
|
362
|
+
assert.equal(Object.hasOwn(attributesSchema.properties, "deletedAt"), true);
|
|
363
|
+
assert.equal(Object.hasOwn(attributesSchema.properties, "lookups"), false);
|
|
364
|
+
assert.equal(
|
|
365
|
+
Array.isArray(resourceSchema.properties.relationships.required),
|
|
366
|
+
false
|
|
367
|
+
);
|
|
368
|
+
assert.equal(
|
|
369
|
+
resourceSchema.properties.relationships.properties.service.properties.data.anyOf[1].type,
|
|
370
|
+
"null"
|
|
371
|
+
);
|
|
372
|
+
assert.ok(Object.hasOwn(contract.responses["200"].transportSchema.properties, "included"));
|
|
373
|
+
});
|
|
374
|
+
|
|
305
375
|
test("createJsonApiResourceRouteTransport wraps tagged meta results for meta routes", () => {
|
|
306
376
|
const contract = createJsonApiResourceRouteContract({
|
|
307
377
|
requestType: "password-changes",
|