@jskit-ai/json-rest-api-core 0.1.2 → 0.1.4
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 +1 -1
- package/package.json +2 -2
- package/src/server/jsonRestApiHost.js +71 -132
- package/test/entrypoints.boundary.test.js +66 -35
package/package.descriptor.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/json-rest-api-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"hooked-api": "1.x.x",
|
|
13
13
|
"json-rest-api": "1.x.x",
|
|
14
|
-
"@jskit-ai/kernel": "0.1.
|
|
14
|
+
"@jskit-ai/kernel": "0.1.59"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -63,6 +63,9 @@ function cloneJsonRestResourceValue(value, { writeSerializers = {} } = {}) {
|
|
|
63
63
|
|
|
64
64
|
if (isPlainJsonRestObject(next.storage)) {
|
|
65
65
|
const serializerKey = normalizeJsonRestText(next.storage.writeSerializer).toLowerCase();
|
|
66
|
+
if (next.storage.virtual === true) {
|
|
67
|
+
next.virtual = true;
|
|
68
|
+
}
|
|
66
69
|
if (serializerKey) {
|
|
67
70
|
const serializer = writeSerializers[serializerKey];
|
|
68
71
|
if (typeof serializer !== "function") {
|
|
@@ -195,16 +198,77 @@ function buildJsonRestQueryParams(resourceType = "", query = {}, { include = und
|
|
|
195
198
|
return queryParams;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
function
|
|
199
|
-
|
|
201
|
+
function extractJsonRestCollectionRows(payload = null) {
|
|
202
|
+
if (Array.isArray(payload)) {
|
|
203
|
+
return payload;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const source = normalizeJsonRestObject(payload);
|
|
207
|
+
return Array.isArray(source.data) ? source.data : [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function extractJsonApiInputRelationships(attributes = {}, resource = null, relationships = null) {
|
|
211
|
+
const normalizedAttributes = {
|
|
212
|
+
...normalizeJsonRestObject(attributes)
|
|
213
|
+
};
|
|
214
|
+
const normalizedRelationships = {
|
|
215
|
+
...normalizeJsonRestObject(relationships)
|
|
216
|
+
};
|
|
217
|
+
const resourceSchema = normalizeJsonRestObject(resource?.schema);
|
|
218
|
+
|
|
219
|
+
for (const [fieldName, fieldDefinition] of Object.entries(resourceSchema)) {
|
|
220
|
+
if (!Object.hasOwn(normalizedAttributes, fieldName)) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const normalizedFieldDefinition = normalizeJsonRestObject(fieldDefinition);
|
|
225
|
+
const relationshipType = normalizeJsonRestText(normalizedFieldDefinition.belongsTo);
|
|
226
|
+
if (!relationshipType) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const relationshipName = normalizeJsonRestText(normalizedFieldDefinition.as, {
|
|
231
|
+
fallback: fieldName
|
|
232
|
+
});
|
|
233
|
+
if (!relationshipName) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const relationshipValue = normalizedAttributes[fieldName];
|
|
238
|
+
delete normalizedAttributes[fieldName];
|
|
239
|
+
|
|
240
|
+
if (relationshipValue === undefined) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!Object.hasOwn(normalizedRelationships, relationshipName)) {
|
|
245
|
+
normalizedRelationships[relationshipName] = createJsonApiRelationship(
|
|
246
|
+
relationshipType,
|
|
247
|
+
relationshipValue
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
attributes: normalizedAttributes,
|
|
254
|
+
relationships: normalizedRelationships
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createJsonApiInputRecord(
|
|
259
|
+
resourceType = "",
|
|
260
|
+
attributes = {},
|
|
261
|
+
{ id = null, relationships = null, resource = null } = {}
|
|
262
|
+
) {
|
|
263
|
+
const normalizedInput = extractJsonApiInputRelationships(attributes, resource, relationships);
|
|
200
264
|
return {
|
|
201
265
|
data: {
|
|
202
266
|
type: normalizeJsonRestText(resourceType),
|
|
203
267
|
...(id == null ? {} : { id: String(id) }),
|
|
204
|
-
attributes:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
268
|
+
attributes: normalizedInput.attributes,
|
|
269
|
+
...(Object.keys(normalizedInput.relationships).length < 1
|
|
270
|
+
? {}
|
|
271
|
+
: { relationships: normalizedInput.relationships })
|
|
208
272
|
}
|
|
209
273
|
};
|
|
210
274
|
}
|
|
@@ -236,131 +300,6 @@ function createJsonRestResourceScopeOptions(resource = {}, { writeSerializers =
|
|
|
236
300
|
return scopeOptions;
|
|
237
301
|
}
|
|
238
302
|
|
|
239
|
-
function normalizeJsonApiResourceObject(resource = {}) {
|
|
240
|
-
const normalizedResource = normalizeJsonRestObject(resource);
|
|
241
|
-
return {
|
|
242
|
-
type: normalizeJsonRestText(normalizedResource.type),
|
|
243
|
-
id: normalizedResource.id == null ? null : String(normalizedResource.id),
|
|
244
|
-
attributes: normalizeJsonRestObject(normalizedResource.attributes),
|
|
245
|
-
relationships: normalizeJsonRestObject(normalizedResource.relationships)
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function buildJsonApiIncludedIndex(payload = {}) {
|
|
250
|
-
const included = Array.isArray(payload?.included) ? payload.included : [];
|
|
251
|
-
const index = new Map();
|
|
252
|
-
|
|
253
|
-
for (const entry of included) {
|
|
254
|
-
const normalizedEntry = normalizeJsonApiResourceObject(entry);
|
|
255
|
-
if (!normalizedEntry.type || !normalizedEntry.id) {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
index.set(`${normalizedEntry.type}:${normalizedEntry.id}`, normalizedEntry);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return index;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function simplifyJsonApiRelationshipData(data, { includedIndex = null, seen = null } = {}) {
|
|
266
|
-
if (Array.isArray(data)) {
|
|
267
|
-
return data
|
|
268
|
-
.map((entry) => simplifyJsonApiRelationshipData(entry, { includedIndex, seen }))
|
|
269
|
-
.filter((entry) => entry != null);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (data == null) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const normalizedReference = normalizeJsonApiResourceObject(data);
|
|
277
|
-
if (!normalizedReference.id) {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const referenceKey =
|
|
282
|
-
normalizedReference.type && normalizedReference.id
|
|
283
|
-
? `${normalizedReference.type}:${normalizedReference.id}`
|
|
284
|
-
: "";
|
|
285
|
-
const nextSeen = seen instanceof Set ? new Set(seen) : new Set();
|
|
286
|
-
|
|
287
|
-
if (referenceKey) {
|
|
288
|
-
if (nextSeen.has(referenceKey)) {
|
|
289
|
-
return {
|
|
290
|
-
id: normalizedReference.id,
|
|
291
|
-
...(normalizedReference.type ? { type: normalizedReference.type } : {})
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
nextSeen.add(referenceKey);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (referenceKey && includedIndex instanceof Map && includedIndex.has(referenceKey)) {
|
|
298
|
-
return simplifyJsonApiResourceObject(includedIndex.get(referenceKey), {
|
|
299
|
-
includedIndex,
|
|
300
|
-
seen: nextSeen
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
id: normalizedReference.id,
|
|
306
|
-
...(normalizedReference.type ? { type: normalizedReference.type } : {})
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function simplifyJsonApiResourceObject(resource = {}, { includedIndex = null, seen = null } = {}) {
|
|
311
|
-
const normalizedResource = normalizeJsonApiResourceObject(resource);
|
|
312
|
-
const resourceKey =
|
|
313
|
-
normalizedResource.type && normalizedResource.id
|
|
314
|
-
? `${normalizedResource.type}:${normalizedResource.id}`
|
|
315
|
-
: "";
|
|
316
|
-
const nextSeen = seen instanceof Set ? new Set(seen) : new Set();
|
|
317
|
-
|
|
318
|
-
if (resourceKey) {
|
|
319
|
-
nextSeen.add(resourceKey);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const simplified = {
|
|
323
|
-
...(normalizedResource.id == null ? {} : { id: normalizedResource.id }),
|
|
324
|
-
...normalizedResource.attributes
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
for (const [relationshipKey, relationshipValue] of Object.entries(normalizedResource.relationships)) {
|
|
328
|
-
if (!relationshipKey || !relationshipValue || !Object.hasOwn(relationshipValue, "data")) {
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
simplified[relationshipKey] = simplifyJsonApiRelationshipData(relationshipValue.data, {
|
|
333
|
-
includedIndex,
|
|
334
|
-
seen: nextSeen
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return simplified;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function simplifyJsonApiDocument(payload = {}) {
|
|
342
|
-
const source = normalizeJsonRestObject(payload);
|
|
343
|
-
const includedIndex = buildJsonApiIncludedIndex(source);
|
|
344
|
-
|
|
345
|
-
if (Array.isArray(source.data)) {
|
|
346
|
-
return source.data.map((entry) => simplifyJsonApiResourceObject(entry, { includedIndex }));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (source.data && typeof source.data === "object") {
|
|
350
|
-
return simplifyJsonApiResourceObject(source.data, { includedIndex });
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (Object.hasOwn(source, "data") && source.data == null) {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (source.meta && typeof source.meta === "object" && !Array.isArray(source.meta)) {
|
|
358
|
-
return source.meta;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return payload;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
303
|
function createJsonRestContext(context = null) {
|
|
365
304
|
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
366
305
|
return {};
|
|
@@ -481,11 +420,11 @@ export {
|
|
|
481
420
|
createJsonApiRelationship,
|
|
482
421
|
createJsonRestResourceScopeOptions,
|
|
483
422
|
createJsonRestContext,
|
|
423
|
+
extractJsonRestCollectionRows,
|
|
484
424
|
isJsonRestResourceMissingError,
|
|
485
425
|
returnNullWhenJsonRestResourceMissing,
|
|
486
426
|
resolveWorkspaceScopeValue,
|
|
487
427
|
resolveUserScopeValue,
|
|
488
|
-
simplifyJsonApiDocument,
|
|
489
428
|
createJsonRestApiHost,
|
|
490
429
|
registerJsonRestApiHost
|
|
491
430
|
};
|
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
createJsonRestResourceScopeOptions,
|
|
13
13
|
createJsonRestContext,
|
|
14
14
|
createJsonRestApiHost,
|
|
15
|
+
extractJsonRestCollectionRows,
|
|
15
16
|
isJsonRestResourceMissingError,
|
|
16
17
|
registerJsonRestApiHost,
|
|
17
18
|
returnNullWhenJsonRestResourceMissing,
|
|
18
19
|
resolveWorkspaceScopeValue,
|
|
19
|
-
resolveUserScopeValue
|
|
20
|
-
simplifyJsonApiDocument
|
|
20
|
+
resolveUserScopeValue
|
|
21
21
|
} from "../src/server/jsonRestApiHost.js";
|
|
22
22
|
import { JsonRestApiCoreServiceProvider } from "../src/server/JsonRestApiCoreServiceProvider.js";
|
|
23
23
|
|
|
@@ -28,6 +28,11 @@ test("package exports include explicit server jsonRestApiHost entrypoint only",
|
|
|
28
28
|
assert.equal(exportsMap["./server"], undefined);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
test("server jsonRestApiHost entrypoint no longer exports host-side JSON:API simplification helpers", async () => {
|
|
32
|
+
const hostModule = await import("../src/server/jsonRestApiHost.js");
|
|
33
|
+
assert.equal(Object.hasOwn(hostModule, "simplifyJsonApiDocument"), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
31
36
|
test("server entrypoint exports shared host helpers", () => {
|
|
32
37
|
assert.equal(INTERNAL_JSON_REST_API, "internal.json-rest-api");
|
|
33
38
|
assert.equal(typeof addResourceIfMissing, "function");
|
|
@@ -37,12 +42,12 @@ test("server entrypoint exports shared host helpers", () => {
|
|
|
37
42
|
assert.equal(typeof createJsonRestResourceScopeOptions, "function");
|
|
38
43
|
assert.equal(typeof createJsonRestContext, "function");
|
|
39
44
|
assert.equal(typeof createJsonRestApiHost, "function");
|
|
45
|
+
assert.equal(typeof extractJsonRestCollectionRows, "function");
|
|
40
46
|
assert.equal(typeof isJsonRestResourceMissingError, "function");
|
|
41
47
|
assert.equal(typeof registerJsonRestApiHost, "function");
|
|
42
48
|
assert.equal(typeof returnNullWhenJsonRestResourceMissing, "function");
|
|
43
49
|
assert.equal(typeof resolveWorkspaceScopeValue, "function");
|
|
44
50
|
assert.equal(typeof resolveUserScopeValue, "function");
|
|
45
|
-
assert.equal(typeof simplifyJsonApiDocument, "function");
|
|
46
51
|
assert.equal(typeof JsonRestApiCoreServiceProvider, "function");
|
|
47
52
|
});
|
|
48
53
|
|
|
@@ -84,6 +89,23 @@ test("createJsonRestContext returns an empty mutable object when source context
|
|
|
84
89
|
assert.equal(result.method, "query");
|
|
85
90
|
});
|
|
86
91
|
|
|
92
|
+
test("extractJsonRestCollectionRows understands the internal collection-document contract", () => {
|
|
93
|
+
const rows = [{ id: "1" }, { id: "2" }];
|
|
94
|
+
|
|
95
|
+
assert.deepEqual(extractJsonRestCollectionRows(rows), rows);
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
extractJsonRestCollectionRows({
|
|
98
|
+
data: rows,
|
|
99
|
+
links: {
|
|
100
|
+
self: "/contacts"
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
rows
|
|
104
|
+
);
|
|
105
|
+
assert.deepEqual(extractJsonRestCollectionRows({ data: null }), []);
|
|
106
|
+
assert.deepEqual(extractJsonRestCollectionRows(null), []);
|
|
107
|
+
});
|
|
108
|
+
|
|
87
109
|
test("createJsonRestApiHost installs normalizeRecordId as the default resource id normalizer", async () => {
|
|
88
110
|
const fakeKnex = Object.assign(() => {}, {
|
|
89
111
|
client: {
|
|
@@ -166,45 +188,41 @@ test("shared query/document helpers build json-rest-api request shapes", () => {
|
|
|
166
188
|
);
|
|
167
189
|
|
|
168
190
|
assert.deepEqual(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
createJsonApiInputRecord("products", {
|
|
192
|
+
serviceId: "9",
|
|
193
|
+
name: "Style Groom"
|
|
194
|
+
}, {
|
|
195
|
+
resource: {
|
|
196
|
+
schema: {
|
|
197
|
+
serviceId: {
|
|
198
|
+
type: "id",
|
|
199
|
+
belongsTo: "services",
|
|
200
|
+
as: "service"
|
|
176
201
|
},
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
data: {
|
|
180
|
-
type: "user-profiles",
|
|
181
|
-
id: "9"
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
],
|
|
187
|
-
included: [
|
|
188
|
-
{
|
|
189
|
-
type: "user-profiles",
|
|
190
|
-
id: "9",
|
|
191
|
-
attributes: {
|
|
192
|
-
displayName: "Chiara"
|
|
202
|
+
name: {
|
|
203
|
+
type: "string"
|
|
193
204
|
}
|
|
194
205
|
}
|
|
195
|
-
|
|
206
|
+
}
|
|
196
207
|
}),
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
{
|
|
209
|
+
data: {
|
|
210
|
+
type: "products",
|
|
211
|
+
attributes: {
|
|
212
|
+
name: "Style Groom"
|
|
213
|
+
},
|
|
214
|
+
relationships: {
|
|
215
|
+
service: {
|
|
216
|
+
data: {
|
|
217
|
+
type: "services",
|
|
218
|
+
id: "9"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
204
221
|
}
|
|
205
222
|
}
|
|
206
|
-
|
|
223
|
+
}
|
|
207
224
|
);
|
|
225
|
+
|
|
208
226
|
});
|
|
209
227
|
|
|
210
228
|
test("createJsonRestResourceScopeOptions clones canonical resource metadata and resolves symbolic write serializers", () => {
|
|
@@ -237,6 +255,17 @@ test("createJsonRestResourceScopeOptions clones canonical resource metadata and
|
|
|
237
255
|
required: true
|
|
238
256
|
})
|
|
239
257
|
})
|
|
258
|
+
}),
|
|
259
|
+
bookingSteps: Object.freeze({
|
|
260
|
+
type: "array",
|
|
261
|
+
storage: Object.freeze({
|
|
262
|
+
virtual: true
|
|
263
|
+
}),
|
|
264
|
+
operations: Object.freeze({
|
|
265
|
+
output: Object.freeze({
|
|
266
|
+
required: false
|
|
267
|
+
})
|
|
268
|
+
})
|
|
240
269
|
})
|
|
241
270
|
}),
|
|
242
271
|
operations: Object.freeze({
|
|
@@ -258,7 +287,9 @@ test("createJsonRestResourceScopeOptions clones canonical resource metadata and
|
|
|
258
287
|
assert.notEqual(result.schema.createdAt, source.schema.createdAt);
|
|
259
288
|
assert.equal(result.schema.createdAt.storage.column, "created_at");
|
|
260
289
|
assert.equal(result.schema.createdAt.storage.serialize, serializer);
|
|
290
|
+
assert.equal(result.schema.createdAt.storage.serialize(null), null);
|
|
261
291
|
assert.equal(result.schema.createdAt.storage.writeSerializer, undefined);
|
|
292
|
+
assert.equal(result.schema.bookingSteps.virtual, true);
|
|
262
293
|
assert.equal(result.normalizeId, normalizeId);
|
|
263
294
|
assert.equal(result.schema.name.maxLength, 190);
|
|
264
295
|
assert.equal(result.schema.name.operations.output.required, true);
|