@jskit-ai/crud-core 0.1.65 → 0.1.66

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/crud-core",
4
- version: "0.1.65",
4
+ version: "0.1.66",
5
5
  kind: "runtime",
6
6
  description: "Shared CRUD helpers used by CRUD modules.",
7
7
  dependsOn: [
@@ -28,7 +28,7 @@ export default Object.freeze({
28
28
  mutations: {
29
29
  dependencies: {
30
30
  runtime: {
31
- "@jskit-ai/crud-core": "0.1.65"
31
+ "@jskit-ai/crud-core": "0.1.66"
32
32
  },
33
33
  dev: {}
34
34
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-core",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -28,15 +28,15 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@tanstack/vue-query": "^5.90.5",
31
- "@jskit-ai/database-runtime": "0.1.57",
32
- "@jskit-ai/http-runtime": "0.1.56",
33
- "@jskit-ai/kernel": "0.1.57",
31
+ "@jskit-ai/database-runtime": "0.1.58",
32
+ "@jskit-ai/http-runtime": "0.1.57",
33
+ "@jskit-ai/kernel": "0.1.58",
34
34
  "json-rest-schema": "1.x.x",
35
- "@jskit-ai/realtime": "0.1.56",
36
- "@jskit-ai/resource-crud-core": "0.1.2",
37
- "@jskit-ai/shell-web": "0.1.56",
38
- "@jskit-ai/users-core": "0.1.67",
39
- "@jskit-ai/users-web": "0.1.72"
35
+ "@jskit-ai/realtime": "0.1.57",
36
+ "@jskit-ai/resource-crud-core": "0.1.3",
37
+ "@jskit-ai/shell-web": "0.1.57",
38
+ "@jskit-ai/users-core": "0.1.68",
39
+ "@jskit-ai/users-web": "0.1.73"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.5.13",
@@ -341,6 +341,10 @@ function mapRecordRow(row, fieldKeys = [], overrides = {}, { recordIdKeys = [] }
341
341
  }
342
342
 
343
343
  const rawValue = row[columnName];
344
+ if (rawValue === undefined) {
345
+ continue;
346
+ }
347
+
344
348
  if (recordIdKeySet.has(normalizedKey)) {
345
349
  const normalizedIdValue = normalizeDbRecordId(rawValue, { fallback: null });
346
350
  mapped[normalizedKey] = normalizedIdValue || rawValue;
@@ -1,4 +1,7 @@
1
- import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
1
+ import {
2
+ createJsonApiResourceObject,
3
+ createJsonApiResourceRouteContract
4
+ } from "@jskit-ai/http-runtime/shared";
2
5
  import {
3
6
  composeSchemaDefinitions,
4
7
  recordIdParamsValidator
@@ -10,6 +13,284 @@ import {
10
13
  createCrudParentFilterQueryValidator
11
14
  } from "./listQueryValidators.js";
12
15
 
16
+ function isRecord(value) {
17
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+
20
+ function resolveSchemaFieldDefinitions(definition = null) {
21
+ const schema = definition?.schema;
22
+ if (!schema || typeof schema.getFieldDefinitions !== "function") {
23
+ return {};
24
+ }
25
+
26
+ const definitions = schema.getFieldDefinitions();
27
+ return isRecord(definitions) ? definitions : {};
28
+ }
29
+
30
+ function resolveJsonApiRelationshipEntries(definition = null) {
31
+ const entries = [];
32
+
33
+ for (const [fieldKey, fieldDefinition] of Object.entries(resolveSchemaFieldDefinitions(definition))) {
34
+ const normalizedFieldDefinition = isRecord(fieldDefinition) ? fieldDefinition : {};
35
+ const relationshipType = String(normalizedFieldDefinition.belongsTo || "").trim();
36
+ if (!relationshipType) {
37
+ continue;
38
+ }
39
+
40
+ const relationshipName = String(normalizedFieldDefinition.as || fieldKey || "").trim();
41
+ if (!relationshipName) {
42
+ continue;
43
+ }
44
+
45
+ entries.push(Object.freeze({
46
+ attributeKey: fieldKey,
47
+ relationshipName,
48
+ relationshipType,
49
+ required: normalizedFieldDefinition.required === true,
50
+ nullable: normalizedFieldDefinition.nullable === true
51
+ }));
52
+ }
53
+
54
+ return Object.freeze(entries);
55
+ }
56
+
57
+ function createRecordAttributesResolver(definition = null, {
58
+ excludeKeys = []
59
+ } = {}) {
60
+ const relationshipEntries = resolveJsonApiRelationshipEntries(definition);
61
+ const excludedKeys = new Set(
62
+ relationshipEntries
63
+ .map((entry) => String(entry.attributeKey || "").trim())
64
+ .filter(Boolean)
65
+ );
66
+
67
+ for (const key of excludeKeys) {
68
+ const normalizedKey = String(key || "").trim();
69
+ if (normalizedKey) {
70
+ excludedKeys.add(normalizedKey);
71
+ }
72
+ }
73
+
74
+ if (excludedKeys.size < 1) {
75
+ return null;
76
+ }
77
+
78
+ return function getRecordAttributes(record = {}) {
79
+ if (!isRecord(record)) {
80
+ return {};
81
+ }
82
+
83
+ const attributes = {
84
+ ...record
85
+ };
86
+ delete attributes.id;
87
+ for (const key of excludedKeys) {
88
+ delete attributes[key];
89
+ }
90
+ return attributes;
91
+ };
92
+ }
93
+
94
+ function createRecordRelationshipsResolver(definition = null) {
95
+ const relationshipEntries = resolveJsonApiRelationshipEntries(definition);
96
+ if (relationshipEntries.length < 1) {
97
+ return null;
98
+ }
99
+
100
+ return function getRecordRelationships(record = {}) {
101
+ if (!isRecord(record)) {
102
+ return undefined;
103
+ }
104
+
105
+ const relationships = {};
106
+
107
+ for (const entry of relationshipEntries) {
108
+ if (!Object.hasOwn(record, entry.attributeKey)) {
109
+ continue;
110
+ }
111
+
112
+ const value = record[entry.attributeKey];
113
+ relationships[entry.relationshipName] = {
114
+ data: value == null || (typeof value === "string" && value.trim() === "")
115
+ ? null
116
+ : {
117
+ type: entry.relationshipType,
118
+ id: String(value)
119
+ }
120
+ };
121
+ }
122
+
123
+ return Object.keys(relationships).length > 0 ? relationships : undefined;
124
+ };
125
+ }
126
+
127
+ function createRequestRelationshipMapper(definition = null) {
128
+ const relationshipEntries = resolveJsonApiRelationshipEntries(definition);
129
+ if (relationshipEntries.length < 1) {
130
+ return null;
131
+ }
132
+
133
+ return function mapRequestRelationships(relationships = {}) {
134
+ const source = isRecord(relationships) ? relationships : {};
135
+ const mapped = {};
136
+
137
+ for (const entry of relationshipEntries) {
138
+ if (!Object.hasOwn(source, entry.relationshipName)) {
139
+ continue;
140
+ }
141
+
142
+ const relationship = isRecord(source[entry.relationshipName])
143
+ ? source[entry.relationshipName]
144
+ : {};
145
+ const data = relationship.data;
146
+
147
+ if (data == null) {
148
+ mapped[entry.attributeKey] = null;
149
+ continue;
150
+ }
151
+
152
+ if (isRecord(data) && data.id != null && String(data.id).trim()) {
153
+ mapped[entry.attributeKey] = data.id;
154
+ }
155
+ }
156
+
157
+ return mapped;
158
+ };
159
+ }
160
+
161
+ function resolveOutputAttributeExcludeKeys(resource = {}) {
162
+ const lookupContainerKey = String(resource?.contract?.lookup?.containerKey || "").trim();
163
+ return lookupContainerKey ? Object.freeze([lookupContainerKey]) : Object.freeze([]);
164
+ }
165
+
166
+ function resolveLookupContainerKey(resource = {}) {
167
+ return String(resource?.contract?.lookup?.containerKey || "").trim();
168
+ }
169
+
170
+ function normalizeLookupId(value) {
171
+ if (value == null) {
172
+ return null;
173
+ }
174
+
175
+ const normalized = String(value).trim();
176
+ return normalized || null;
177
+ }
178
+
179
+ function normalizeIncludedLookupRecord(source = null, fallbackId = null) {
180
+ if (!isRecord(source)) {
181
+ return null;
182
+ }
183
+
184
+ const directId = normalizeLookupId(source.id);
185
+ if (directId) {
186
+ return {
187
+ id: directId,
188
+ ...source
189
+ };
190
+ }
191
+
192
+ const preferredNestedRecord = normalizeLookupId(fallbackId) && isRecord(source[fallbackId])
193
+ ? source[fallbackId]
194
+ : null;
195
+ if (preferredNestedRecord) {
196
+ return {
197
+ id: normalizeLookupId(preferredNestedRecord.id ?? fallbackId),
198
+ ...preferredNestedRecord
199
+ };
200
+ }
201
+
202
+ const nestedEntries = Object.entries(source).filter(([, value]) => isRecord(value));
203
+ if (nestedEntries.length !== 1) {
204
+ const fallbackRecordId = normalizeLookupId(fallbackId);
205
+ if (!fallbackRecordId) {
206
+ return null;
207
+ }
208
+
209
+ return {
210
+ id: fallbackRecordId,
211
+ ...source
212
+ };
213
+ }
214
+
215
+ const [nestedKey, nestedRecord] = nestedEntries[0];
216
+ const nestedId = normalizeLookupId(nestedRecord.id ?? nestedKey ?? fallbackId);
217
+ if (!nestedId) {
218
+ return null;
219
+ }
220
+
221
+ return {
222
+ id: nestedId,
223
+ ...nestedRecord
224
+ };
225
+ }
226
+
227
+ function createLookupIncludedResolver(definition = null, {
228
+ lookupContainerKey = ""
229
+ } = {}) {
230
+ const relationshipEntries = resolveJsonApiRelationshipEntries(definition);
231
+ const normalizedLookupContainerKey = String(lookupContainerKey || "").trim();
232
+ if (!normalizedLookupContainerKey || relationshipEntries.length < 1) {
233
+ return null;
234
+ }
235
+
236
+ return function getIncluded(payload = {}) {
237
+ const sourceRecords = Array.isArray(payload)
238
+ ? payload
239
+ : Array.isArray(payload?.items)
240
+ ? payload.items
241
+ : isRecord(payload)
242
+ ? [payload]
243
+ : [];
244
+ const included = [];
245
+ const seen = new Set();
246
+
247
+ for (const record of sourceRecords) {
248
+ if (!isRecord(record)) {
249
+ continue;
250
+ }
251
+
252
+ const sourceLookups = isRecord(record[normalizedLookupContainerKey])
253
+ ? record[normalizedLookupContainerKey]
254
+ : {};
255
+
256
+ for (const entry of relationshipEntries) {
257
+ const relationshipId = normalizeLookupId(record[entry.attributeKey]);
258
+ const lookupRecord = normalizeIncludedLookupRecord(
259
+ sourceLookups[entry.attributeKey] ?? sourceLookups[entry.relationshipName],
260
+ relationshipId
261
+ );
262
+ const includedId = normalizeLookupId(lookupRecord?.id ?? relationshipId);
263
+ if (!lookupRecord || !includedId) {
264
+ continue;
265
+ }
266
+
267
+ const resourceKey = `${entry.relationshipType}:${includedId}`;
268
+ if (seen.has(resourceKey)) {
269
+ continue;
270
+ }
271
+ seen.add(resourceKey);
272
+
273
+ const attributes = {
274
+ ...lookupRecord
275
+ };
276
+ delete attributes.id;
277
+ delete attributes.type;
278
+ delete attributes.relationships;
279
+ delete attributes.links;
280
+ delete attributes.meta;
281
+
282
+ included.push(createJsonApiResourceObject({
283
+ type: entry.relationshipType,
284
+ id: includedId,
285
+ attributes
286
+ }));
287
+ }
288
+ }
289
+
290
+ return included;
291
+ };
292
+ }
293
+
13
294
  function createCrudJsonApiRouteContracts({
14
295
  resource = {},
15
296
  routeParamsValidator = null,
@@ -33,34 +314,93 @@ function createCrudJsonApiRouteContracts({
33
314
  ])
34
315
  : recordIdParamsValidator;
35
316
  const routeType = resource?.namespace;
317
+ const viewOutput = resource?.operations?.view?.output;
318
+ const createBody = resource?.operations?.create?.body;
319
+ const createOutput = resource?.operations?.create?.output;
320
+ const patchBody = resource?.operations?.patch?.body;
321
+ const patchOutput = resource?.operations?.patch?.output;
322
+ const outputAttributeExcludeKeys = resolveOutputAttributeExcludeKeys(resource);
323
+ const lookupContainerKey = resolveLookupContainerKey(resource);
324
+ const viewOutputRelationships = resolveJsonApiRelationshipEntries(viewOutput);
325
+ const createBodyRelationships = resolveJsonApiRelationshipEntries(createBody);
326
+ const createOutputRelationships = resolveJsonApiRelationshipEntries(createOutput);
327
+ const patchBodyRelationships = resolveJsonApiRelationshipEntries(patchBody);
328
+ const patchOutputRelationships = resolveJsonApiRelationshipEntries(patchOutput);
329
+ const viewRecordAttributes = createRecordAttributesResolver(viewOutput, {
330
+ excludeKeys: outputAttributeExcludeKeys
331
+ });
332
+ const viewRecordRelationships = createRecordRelationshipsResolver(viewOutput);
333
+ const createRecordAttributes = createRecordAttributesResolver(createOutput, {
334
+ excludeKeys: outputAttributeExcludeKeys
335
+ });
336
+ const createRecordRelationships = createRecordRelationshipsResolver(createOutput);
337
+ const patchRecordAttributes = createRecordAttributesResolver(patchOutput, {
338
+ excludeKeys: outputAttributeExcludeKeys
339
+ });
340
+ const patchRecordRelationships = createRecordRelationshipsResolver(patchOutput);
341
+ const createRequestRelationships = createRequestRelationshipMapper(createBody);
342
+ const patchRequestRelationships = createRequestRelationshipMapper(patchBody);
343
+ const viewIncluded = createLookupIncludedResolver(viewOutput, {
344
+ lookupContainerKey
345
+ });
346
+ const createIncluded = createLookupIncludedResolver(createOutput, {
347
+ lookupContainerKey
348
+ });
349
+ const patchIncluded = createLookupIncludedResolver(patchOutput, {
350
+ lookupContainerKey
351
+ });
36
352
 
37
353
  return Object.freeze({
38
354
  listRouteContract: createJsonApiResourceRouteContract({
39
355
  type: routeType,
40
356
  query: listRouteQueryValidator,
41
- output: resource?.operations?.view?.output,
42
- outputKind: "collection"
357
+ output: viewOutput,
358
+ outputKind: "collection",
359
+ outputAttributeExcludeKeys,
360
+ outputRelationshipEntries: viewOutputRelationships,
361
+ getRecordAttributes: viewRecordAttributes,
362
+ getRecordRelationships: viewRecordRelationships,
363
+ getIncluded: viewIncluded
43
364
  }),
44
365
  viewRouteContract: createJsonApiResourceRouteContract({
45
366
  type: routeType,
46
367
  query: lookupIncludeQueryValidator,
47
- output: resource?.operations?.view?.output,
48
- outputKind: "record"
368
+ output: viewOutput,
369
+ outputKind: "record",
370
+ outputAttributeExcludeKeys,
371
+ outputRelationshipEntries: viewOutputRelationships,
372
+ getRecordAttributes: viewRecordAttributes,
373
+ getRecordRelationships: viewRecordRelationships,
374
+ getIncluded: viewIncluded
49
375
  }),
50
376
  createRouteContract: createJsonApiResourceRouteContract({
51
377
  type: routeType,
52
- body: resource?.operations?.create?.body,
53
- output: resource?.operations?.create?.output,
378
+ body: createBody,
379
+ output: createOutput,
54
380
  outputKind: "record",
55
381
  successStatus: 201,
56
- includeValidation400: true
382
+ includeValidation400: true,
383
+ bodyRelationshipEntries: createBodyRelationships,
384
+ outputAttributeExcludeKeys,
385
+ outputRelationshipEntries: createOutputRelationships,
386
+ mapRequestRelationships: createRequestRelationships,
387
+ getRecordAttributes: createRecordAttributes,
388
+ getRecordRelationships: createRecordRelationships,
389
+ getIncluded: createIncluded
57
390
  }),
58
391
  updateRouteContract: createJsonApiResourceRouteContract({
59
392
  type: routeType,
60
- body: resource?.operations?.patch?.body,
61
- output: resource?.operations?.patch?.output,
393
+ body: patchBody,
394
+ output: patchOutput,
62
395
  outputKind: "record",
63
- includeValidation400: true
396
+ includeValidation400: true,
397
+ bodyRelationshipEntries: patchBodyRelationships,
398
+ outputAttributeExcludeKeys,
399
+ outputRelationshipEntries: patchOutputRelationships,
400
+ mapRequestRelationships: patchRequestRelationships,
401
+ getRecordAttributes: patchRecordAttributes,
402
+ getRecordRelationships: patchRecordRelationships,
403
+ getIncluded: patchIncluded
64
404
  }),
65
405
  deleteRouteContract: createJsonApiResourceRouteContract({
66
406
  type: routeType,
@@ -126,6 +126,18 @@ test("mapRecordRow remaps rows by key/column pairs", () => {
126
126
  });
127
127
  });
128
128
 
129
+ test("mapRecordRow omits keys whose source column is absent", () => {
130
+ const row = { some_column: 1 };
131
+ const mapped = mapRecordRow(row, ["someKey", "virtualField"], {
132
+ someKey: "some_column",
133
+ virtualField: "virtual_field"
134
+ });
135
+
136
+ assert.deepEqual(mapped, {
137
+ someKey: 1
138
+ });
139
+ });
140
+
129
141
  test("buildWritePayload respects defined keys", () => {
130
142
  const payload = buildWritePayload(
131
143
  { foo: "bar", missing: true },
@@ -4,6 +4,7 @@ import { createSchema } from "json-rest-schema";
4
4
  import {
5
5
  validateSchemaPayload
6
6
  } from "@jskit-ai/kernel/shared/validators";
7
+ import { returnJsonApiData } from "@jskit-ai/http-runtime/shared/validators/jsonApiResult";
7
8
  import { createCrudJsonApiRouteContracts } from "../src/server/routeContracts.js";
8
9
 
9
10
  function createSchemaDefinition(structure = {}, mode = "patch") {
@@ -17,6 +18,11 @@ function createCrudResource() {
17
18
  return Object.freeze({
18
19
  namespace: "contacts",
19
20
  defaultSort: Object.freeze(["-createdAt"]),
21
+ contract: Object.freeze({
22
+ lookup: Object.freeze({
23
+ containerKey: "lookups"
24
+ })
25
+ }),
20
26
  operations: Object.freeze({
21
27
  view: Object.freeze({
22
28
  output: createSchemaDefinition({
@@ -24,6 +30,13 @@ function createCrudResource() {
24
30
  type: "string",
25
31
  required: true
26
32
  },
33
+ ownerUserId: {
34
+ type: "string",
35
+ required: false,
36
+ nullable: true,
37
+ belongsTo: "userProfiles",
38
+ as: "owner"
39
+ },
27
40
  contactId: {
28
41
  type: "string",
29
42
  required: false,
@@ -36,11 +49,22 @@ function createCrudResource() {
36
49
  name: {
37
50
  type: "string",
38
51
  required: true
52
+ },
53
+ lookups: {
54
+ type: "object",
55
+ required: false
39
56
  }
40
57
  }, "replace")
41
58
  }),
42
59
  create: Object.freeze({
43
60
  body: createSchemaDefinition({
61
+ ownerUserId: {
62
+ type: "string",
63
+ required: false,
64
+ nullable: true,
65
+ belongsTo: "userProfiles",
66
+ as: "owner"
67
+ },
44
68
  contactId: {
45
69
  type: "string",
46
70
  required: false,
@@ -60,14 +84,32 @@ function createCrudResource() {
60
84
  type: "string",
61
85
  required: true
62
86
  },
87
+ ownerUserId: {
88
+ type: "string",
89
+ required: false,
90
+ nullable: true,
91
+ belongsTo: "userProfiles",
92
+ as: "owner"
93
+ },
63
94
  name: {
64
95
  type: "string",
65
96
  required: true
97
+ },
98
+ lookups: {
99
+ type: "object",
100
+ required: false
66
101
  }
67
102
  }, "replace")
68
103
  }),
69
104
  patch: Object.freeze({
70
105
  body: createSchemaDefinition({
106
+ ownerUserId: {
107
+ type: "string",
108
+ required: false,
109
+ nullable: true,
110
+ belongsTo: "userProfiles",
111
+ as: "owner"
112
+ },
71
113
  name: {
72
114
  type: "string",
73
115
  required: false
@@ -78,9 +120,20 @@ function createCrudResource() {
78
120
  type: "string",
79
121
  required: true
80
122
  },
123
+ ownerUserId: {
124
+ type: "string",
125
+ required: false,
126
+ nullable: true,
127
+ belongsTo: "userProfiles",
128
+ as: "owner"
129
+ },
81
130
  name: {
82
131
  type: "string",
83
132
  required: true
133
+ },
134
+ lookups: {
135
+ type: "object",
136
+ required: false
84
137
  }
85
138
  }, "replace")
86
139
  })
@@ -109,6 +162,7 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
109
162
  assert.equal(contracts.listRouteContract.responses[200].transportSchema.type, "object");
110
163
  assert.equal(contracts.createRouteContract.responses[201].transportSchema.type, "object");
111
164
  assert.equal(contracts.updateRouteContract.responses[200].transportSchema.type, "object");
165
+ assert.ok(Object.hasOwn(contracts.viewRouteContract.responses[200].transportSchema.properties, "included"));
112
166
  assert.equal(Object.hasOwn(contracts.deleteRouteContract.responses, "204"), false);
113
167
  assert.equal(contracts.createRouteContract.body, resource.operations.create.body);
114
168
  assert.equal(contracts.updateRouteContract.body, resource.operations.patch.body);
@@ -132,6 +186,102 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
132
186
  cursor: "offset:2",
133
187
  limit: 25
134
188
  });
189
+
190
+ const decodedCreateBody = contracts.createRouteContract.transport.request.body({
191
+ data: {
192
+ type: "contacts",
193
+ attributes: {
194
+ name: "Alice"
195
+ },
196
+ relationships: {
197
+ owner: {
198
+ data: {
199
+ type: "userProfiles",
200
+ id: "user-7"
201
+ }
202
+ }
203
+ }
204
+ }
205
+ });
206
+
207
+ assert.deepEqual(decodedCreateBody, {
208
+ name: "Alice",
209
+ ownerUserId: "user-7"
210
+ });
211
+
212
+ const responseDocument = contracts.viewRouteContract.transport.response(returnJsonApiData({
213
+ id: "contact-1",
214
+ ownerUserId: "user-7",
215
+ name: "Alice",
216
+ lookups: {
217
+ owner: {
218
+ "user-7": {
219
+ id: "user-7",
220
+ name: "User Seven"
221
+ }
222
+ }
223
+ }
224
+ }));
225
+
226
+ assert.deepEqual(responseDocument.data.attributes, {
227
+ name: "Alice"
228
+ });
229
+ assert.deepEqual(responseDocument.data.relationships, {
230
+ owner: {
231
+ data: {
232
+ type: "userProfiles",
233
+ id: "user-7"
234
+ }
235
+ }
236
+ });
237
+ assert.deepEqual(responseDocument.included, [
238
+ {
239
+ type: "userProfiles",
240
+ id: "user-7",
241
+ attributes: {
242
+ name: "User Seven"
243
+ }
244
+ }
245
+ ]);
246
+
247
+ const listResponseDocument = contracts.listRouteContract.transport.response(returnJsonApiData({
248
+ items: [
249
+ {
250
+ id: "contact-1",
251
+ ownerUserId: "user-7",
252
+ name: "Alice",
253
+ lookups: {
254
+ owner: {
255
+ "user-7": {
256
+ id: "user-7",
257
+ name: "User Seven"
258
+ }
259
+ }
260
+ }
261
+ },
262
+ {
263
+ id: "contact-2",
264
+ ownerUserId: "user-7",
265
+ name: "Bob",
266
+ lookups: {
267
+ ownerUserId: {
268
+ id: "user-7",
269
+ name: "User Seven"
270
+ }
271
+ }
272
+ }
273
+ ]
274
+ }));
275
+
276
+ assert.deepEqual(listResponseDocument.included, [
277
+ {
278
+ type: "userProfiles",
279
+ id: "user-7",
280
+ attributes: {
281
+ name: "User Seven"
282
+ }
283
+ }
284
+ ]);
135
285
  });
136
286
 
137
287
  test("createCrudJsonApiRouteContracts falls back to recordId params when no route params validator is provided", () => {