@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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/json-rest-api-core",
4
- version: "0.1.2",
4
+ version: "0.1.4",
5
5
  kind: "runtime",
6
6
  description: "Shared internal json-rest-api host runtime for JSKIT server packages.",
7
7
  dependsOn: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/json-rest-api-core",
3
- "version": "0.1.2",
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.57"
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 createJsonApiInputRecord(resourceType = "", attributes = {}, { id = null, relationships = null } = {}) {
199
- const normalizedRelationships = normalizeJsonRestObject(relationships);
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
- ...normalizeJsonRestObject(attributes)
206
- },
207
- ...(Object.keys(normalizedRelationships).length < 1 ? {} : { relationships: normalizedRelationships })
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
- simplifyJsonApiDocument({
170
- data: [
171
- {
172
- type: "workspace-memberships",
173
- id: "11",
174
- attributes: {
175
- roleSid: "owner"
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
- relationships: {
178
- user: {
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
- id: "11",
200
- roleSid: "owner",
201
- user: {
202
- id: "9",
203
- displayName: "Chiara"
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);