@jskit-ai/crud-core 0.1.92 → 0.1.93
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 +2 -2
- package/package.json +9 -9
- package/src/server/routeContracts.js +206 -54
- package/test/routeContracts.test.js +126 -0
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
4
|
+
version: "0.1.93",
|
|
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.
|
|
31
|
+
"@jskit-ai/crud-core": "0.1.93"
|
|
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.
|
|
3
|
+
"version": "0.1.93",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"./server/routeContracts": "./src/server/routeContracts.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
31
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
32
|
-
"@jskit-ai/kernel": "0.1.
|
|
30
|
+
"@jskit-ai/database-runtime": "0.1.85",
|
|
31
|
+
"@jskit-ai/http-runtime": "0.1.84",
|
|
32
|
+
"@jskit-ai/kernel": "0.1.85",
|
|
33
33
|
"json-rest-schema": "1.x.x",
|
|
34
|
-
"@jskit-ai/realtime": "0.1.
|
|
35
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
36
|
-
"@jskit-ai/shell-web": "0.1.
|
|
37
|
-
"@jskit-ai/users-core": "0.1.
|
|
38
|
-
"@jskit-ai/users-web": "0.1.
|
|
34
|
+
"@jskit-ai/realtime": "0.1.84",
|
|
35
|
+
"@jskit-ai/resource-crud-core": "0.1.30",
|
|
36
|
+
"@jskit-ai/shell-web": "0.1.84",
|
|
37
|
+
"@jskit-ai/users-core": "0.1.95",
|
|
38
|
+
"@jskit-ai/users-web": "0.1.100"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@tanstack/vue-query": "^5.90.5",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
composeSchemaDefinitions,
|
|
7
7
|
recordIdParamsValidator
|
|
8
8
|
} from "@jskit-ai/kernel/shared/validators";
|
|
9
|
+
import { resolveCrudResourceScopeName } from "@jskit-ai/kernel/shared/support/crudLookup";
|
|
9
10
|
import {
|
|
10
11
|
createCrudCursorPaginationQueryValidator,
|
|
11
12
|
listSearchQueryValidator as defaultListSearchQueryValidator,
|
|
@@ -33,19 +34,48 @@ function resolveJsonApiRelationshipEntries(definition = null) {
|
|
|
33
34
|
for (const [fieldKey, fieldDefinition] of Object.entries(resolveSchemaFieldDefinitions(definition))) {
|
|
34
35
|
const normalizedFieldDefinition = isRecord(fieldDefinition) ? fieldDefinition : {};
|
|
35
36
|
const relationshipType = String(normalizedFieldDefinition.belongsTo || "").trim();
|
|
36
|
-
if (
|
|
37
|
+
if (relationshipType) {
|
|
38
|
+
const relationshipName = String(normalizedFieldDefinition.as || fieldKey || "").trim();
|
|
39
|
+
if (!relationshipName) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
entries.push(Object.freeze({
|
|
44
|
+
attributeKey: fieldKey,
|
|
45
|
+
relationshipName,
|
|
46
|
+
relationshipType,
|
|
47
|
+
required: normalizedFieldDefinition.required === true,
|
|
48
|
+
nullable: normalizedFieldDefinition.nullable === true
|
|
49
|
+
}));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const relation = isRecord(normalizedFieldDefinition.relation)
|
|
54
|
+
? normalizedFieldDefinition.relation
|
|
55
|
+
: {};
|
|
56
|
+
if (String(relation.kind || "").trim().toLowerCase() !== "collection") {
|
|
37
57
|
continue;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
const
|
|
41
|
-
|
|
60
|
+
const collectionRelationshipType = resolveCrudResourceScopeName(
|
|
61
|
+
relation.target || relation.targetResource || relation.namespace || relation.apiPath
|
|
62
|
+
);
|
|
63
|
+
if (!collectionRelationshipType) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const collectionRelationshipName = String(
|
|
68
|
+
relation.as || normalizedFieldDefinition.as || fieldKey || ""
|
|
69
|
+
).trim();
|
|
70
|
+
if (!collectionRelationshipName) {
|
|
42
71
|
continue;
|
|
43
72
|
}
|
|
44
73
|
|
|
45
74
|
entries.push(Object.freeze({
|
|
46
75
|
attributeKey: fieldKey,
|
|
47
|
-
relationshipName,
|
|
48
|
-
relationshipType,
|
|
76
|
+
relationshipName: collectionRelationshipName,
|
|
77
|
+
relationshipType: collectionRelationshipType,
|
|
78
|
+
many: true,
|
|
49
79
|
required: normalizedFieldDefinition.required === true,
|
|
50
80
|
nullable: normalizedFieldDefinition.nullable === true
|
|
51
81
|
}));
|
|
@@ -54,6 +84,79 @@ function resolveJsonApiRelationshipEntries(definition = null) {
|
|
|
54
84
|
return Object.freeze(entries);
|
|
55
85
|
}
|
|
56
86
|
|
|
87
|
+
function readOwnValue(source = {}, key = "") {
|
|
88
|
+
if (isRecord(source) && Object.hasOwn(source, key)) {
|
|
89
|
+
return {
|
|
90
|
+
found: true,
|
|
91
|
+
value: source[key]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
found: false,
|
|
97
|
+
value: undefined
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveLookupContainer(record = {}, lookupContainerKey = "") {
|
|
102
|
+
const normalizedLookupContainerKey = String(lookupContainerKey || "").trim();
|
|
103
|
+
if (!normalizedLookupContainerKey || !isRecord(record?.[normalizedLookupContainerKey])) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return record[normalizedLookupContainerKey];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveRelationshipValueSource(record = {}, entry = {}, {
|
|
111
|
+
lookupContainerKey = "",
|
|
112
|
+
preferLookup = false
|
|
113
|
+
} = {}) {
|
|
114
|
+
const lookups = resolveLookupContainer(record, lookupContainerKey);
|
|
115
|
+
const lookupValueByAttributeKey = readOwnValue(lookups, entry.attributeKey);
|
|
116
|
+
const lookupValue = lookupValueByAttributeKey.found
|
|
117
|
+
? lookupValueByAttributeKey
|
|
118
|
+
: readOwnValue(lookups, entry.relationshipName);
|
|
119
|
+
const recordValue = readOwnValue(record, entry.attributeKey);
|
|
120
|
+
|
|
121
|
+
if (preferLookup) {
|
|
122
|
+
return lookupValue.found ? lookupValue : recordValue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return recordValue.found ? recordValue : lookupValue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeLookupId(value) {
|
|
129
|
+
if (value == null) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const normalized = String(value).trim();
|
|
134
|
+
return normalized || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeRelationshipIdentifierId(value = null) {
|
|
138
|
+
if (isRecord(value)) {
|
|
139
|
+
if (isRecord(value.data)) {
|
|
140
|
+
return normalizeLookupId(value.data.id);
|
|
141
|
+
}
|
|
142
|
+
return normalizeLookupId(value.id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return normalizeLookupId(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createRelationshipIdentifier(entry = {}, value = null) {
|
|
149
|
+
const id = normalizeRelationshipIdentifierId(value);
|
|
150
|
+
if (!id) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
type: entry.relationshipType,
|
|
156
|
+
id
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
57
160
|
function createRecordAttributesResolver(definition = null, {
|
|
58
161
|
excludeKeys = []
|
|
59
162
|
} = {}) {
|
|
@@ -91,7 +194,9 @@ function createRecordAttributesResolver(definition = null, {
|
|
|
91
194
|
};
|
|
92
195
|
}
|
|
93
196
|
|
|
94
|
-
function createRecordRelationshipsResolver(definition = null
|
|
197
|
+
function createRecordRelationshipsResolver(definition = null, {
|
|
198
|
+
lookupContainerKey = ""
|
|
199
|
+
} = {}) {
|
|
95
200
|
const relationshipEntries = resolveJsonApiRelationshipEntries(definition);
|
|
96
201
|
if (relationshipEntries.length < 1) {
|
|
97
202
|
return null;
|
|
@@ -105,18 +210,27 @@ function createRecordRelationshipsResolver(definition = null) {
|
|
|
105
210
|
const relationships = {};
|
|
106
211
|
|
|
107
212
|
for (const entry of relationshipEntries) {
|
|
108
|
-
|
|
213
|
+
const relationshipSource = resolveRelationshipValueSource(record, entry, {
|
|
214
|
+
lookupContainerKey,
|
|
215
|
+
preferLookup: entry.many === true
|
|
216
|
+
});
|
|
217
|
+
if (!relationshipSource.found) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const value = relationshipSource.value;
|
|
222
|
+
if (entry.many === true) {
|
|
223
|
+
const items = Array.isArray(value) ? value : [];
|
|
224
|
+
relationships[entry.relationshipName] = {
|
|
225
|
+
data: items
|
|
226
|
+
.map((item) => createRelationshipIdentifier(entry, item))
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
};
|
|
109
229
|
continue;
|
|
110
230
|
}
|
|
111
231
|
|
|
112
|
-
const value = record[entry.attributeKey];
|
|
113
232
|
relationships[entry.relationshipName] = {
|
|
114
|
-
data:
|
|
115
|
-
? null
|
|
116
|
-
: {
|
|
117
|
-
type: entry.relationshipType,
|
|
118
|
-
id: String(value)
|
|
119
|
-
}
|
|
233
|
+
data: createRelationshipIdentifier(entry, value)
|
|
120
234
|
};
|
|
121
235
|
}
|
|
122
236
|
|
|
@@ -145,7 +259,16 @@ function createRequestRelationshipMapper(definition = null) {
|
|
|
145
259
|
const data = relationship.data;
|
|
146
260
|
|
|
147
261
|
if (data == null) {
|
|
148
|
-
mapped[entry.attributeKey] = null;
|
|
262
|
+
mapped[entry.attributeKey] = entry.many === true ? [] : null;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (entry.many === true) {
|
|
267
|
+
mapped[entry.attributeKey] = Array.isArray(data)
|
|
268
|
+
? data
|
|
269
|
+
.map((item) => normalizeRelationshipIdentifierId(item))
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
: [];
|
|
149
272
|
continue;
|
|
150
273
|
}
|
|
151
274
|
|
|
@@ -167,15 +290,6 @@ function resolveLookupContainerKey(resource = {}) {
|
|
|
167
290
|
return String(resource?.contract?.lookup?.containerKey || "").trim();
|
|
168
291
|
}
|
|
169
292
|
|
|
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
293
|
function normalizeIncludedLookupRecord(source = null, fallbackId = null) {
|
|
180
294
|
if (!isRecord(source)) {
|
|
181
295
|
return null;
|
|
@@ -224,6 +338,41 @@ function normalizeIncludedLookupRecord(source = null, fallbackId = null) {
|
|
|
224
338
|
};
|
|
225
339
|
}
|
|
226
340
|
|
|
341
|
+
function appendIncludedRelationshipResource({
|
|
342
|
+
included = [],
|
|
343
|
+
seen = new Set(),
|
|
344
|
+
entry = {},
|
|
345
|
+
lookupRecord = null,
|
|
346
|
+
fallbackId = null
|
|
347
|
+
} = {}) {
|
|
348
|
+
const normalizedLookupRecord = normalizeIncludedLookupRecord(lookupRecord, fallbackId);
|
|
349
|
+
const includedId = normalizeLookupId(normalizedLookupRecord?.id ?? fallbackId);
|
|
350
|
+
if (!normalizedLookupRecord || !includedId) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const resourceKey = `${entry.relationshipType}:${includedId}`;
|
|
355
|
+
if (seen.has(resourceKey)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
seen.add(resourceKey);
|
|
359
|
+
|
|
360
|
+
const attributes = {
|
|
361
|
+
...normalizedLookupRecord
|
|
362
|
+
};
|
|
363
|
+
delete attributes.id;
|
|
364
|
+
delete attributes.type;
|
|
365
|
+
delete attributes.relationships;
|
|
366
|
+
delete attributes.links;
|
|
367
|
+
delete attributes.meta;
|
|
368
|
+
|
|
369
|
+
included.push(createJsonApiResourceObject({
|
|
370
|
+
type: entry.relationshipType,
|
|
371
|
+
id: includedId,
|
|
372
|
+
attributes
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
|
|
227
376
|
function createLookupIncludedResolver(definition = null, {
|
|
228
377
|
lookupContainerKey = ""
|
|
229
378
|
} = {}) {
|
|
@@ -254,36 +403,33 @@ function createLookupIncludedResolver(definition = null, {
|
|
|
254
403
|
: {};
|
|
255
404
|
|
|
256
405
|
for (const entry of relationshipEntries) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
406
|
+
if (entry.many === true) {
|
|
407
|
+
const relationshipSource = resolveRelationshipValueSource(record, entry, {
|
|
408
|
+
lookupContainerKey: normalizedLookupContainerKey,
|
|
409
|
+
preferLookup: true
|
|
410
|
+
});
|
|
411
|
+
const lookupRecords = Array.isArray(relationshipSource.value) ? relationshipSource.value : [];
|
|
412
|
+
for (const lookupRecord of lookupRecords) {
|
|
413
|
+
appendIncludedRelationshipResource({
|
|
414
|
+
included,
|
|
415
|
+
seen,
|
|
416
|
+
entry,
|
|
417
|
+
lookupRecord,
|
|
418
|
+
fallbackId: lookupRecord?.id
|
|
419
|
+
});
|
|
420
|
+
}
|
|
264
421
|
continue;
|
|
265
422
|
}
|
|
266
423
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
}));
|
|
424
|
+
const relationshipId = normalizeLookupId(record[entry.attributeKey]);
|
|
425
|
+
const lookupRecord = sourceLookups[entry.attributeKey] ?? sourceLookups[entry.relationshipName];
|
|
426
|
+
appendIncludedRelationshipResource({
|
|
427
|
+
included,
|
|
428
|
+
seen,
|
|
429
|
+
entry,
|
|
430
|
+
lookupRecord,
|
|
431
|
+
fallbackId: relationshipId
|
|
432
|
+
});
|
|
287
433
|
}
|
|
288
434
|
}
|
|
289
435
|
|
|
@@ -329,15 +475,21 @@ function createCrudJsonApiRouteContracts({
|
|
|
329
475
|
const viewRecordAttributes = createRecordAttributesResolver(viewOutput, {
|
|
330
476
|
excludeKeys: outputAttributeExcludeKeys
|
|
331
477
|
});
|
|
332
|
-
const viewRecordRelationships = createRecordRelationshipsResolver(viewOutput
|
|
478
|
+
const viewRecordRelationships = createRecordRelationshipsResolver(viewOutput, {
|
|
479
|
+
lookupContainerKey
|
|
480
|
+
});
|
|
333
481
|
const createRecordAttributes = createRecordAttributesResolver(createOutput, {
|
|
334
482
|
excludeKeys: outputAttributeExcludeKeys
|
|
335
483
|
});
|
|
336
|
-
const createRecordRelationships = createRecordRelationshipsResolver(createOutput
|
|
484
|
+
const createRecordRelationships = createRecordRelationshipsResolver(createOutput, {
|
|
485
|
+
lookupContainerKey
|
|
486
|
+
});
|
|
337
487
|
const patchRecordAttributes = createRecordAttributesResolver(patchOutput, {
|
|
338
488
|
excludeKeys: outputAttributeExcludeKeys
|
|
339
489
|
});
|
|
340
|
-
const patchRecordRelationships = createRecordRelationshipsResolver(patchOutput
|
|
490
|
+
const patchRecordRelationships = createRecordRelationshipsResolver(patchOutput, {
|
|
491
|
+
lookupContainerKey
|
|
492
|
+
});
|
|
341
493
|
const createRequestRelationships = createRequestRelationshipMapper(createBody);
|
|
342
494
|
const patchRequestRelationships = createRequestRelationshipMapper(patchBody);
|
|
343
495
|
const viewIncluded = createLookupIncludedResolver(viewOutput, {
|
|
@@ -244,6 +244,26 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
|
|
|
244
244
|
}
|
|
245
245
|
]);
|
|
246
246
|
|
|
247
|
+
const lookupOnlyRelationshipDocument = contracts.viewRouteContract.transport.response(returnJsonApiData({
|
|
248
|
+
id: "contact-3",
|
|
249
|
+
name: "Charlie",
|
|
250
|
+
lookups: {
|
|
251
|
+
owner: {
|
|
252
|
+
id: "user-8",
|
|
253
|
+
name: "User Eight"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
assert.deepEqual(lookupOnlyRelationshipDocument.data.relationships, {
|
|
259
|
+
owner: {
|
|
260
|
+
data: {
|
|
261
|
+
type: "userProfiles",
|
|
262
|
+
id: "user-8"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
247
267
|
const listResponseDocument = contracts.listRouteContract.transport.response(returnJsonApiData({
|
|
248
268
|
items: [
|
|
249
269
|
{
|
|
@@ -284,6 +304,112 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
|
|
|
284
304
|
]);
|
|
285
305
|
});
|
|
286
306
|
|
|
307
|
+
test("createCrudJsonApiRouteContracts serializes collection relationships from hydrated lookups", () => {
|
|
308
|
+
const output = createSchemaDefinition({
|
|
309
|
+
id: {
|
|
310
|
+
type: "string",
|
|
311
|
+
required: true
|
|
312
|
+
},
|
|
313
|
+
name: {
|
|
314
|
+
type: "string",
|
|
315
|
+
required: true
|
|
316
|
+
},
|
|
317
|
+
pets: {
|
|
318
|
+
type: "array",
|
|
319
|
+
required: false,
|
|
320
|
+
relation: {
|
|
321
|
+
kind: "collection",
|
|
322
|
+
namespace: "pets",
|
|
323
|
+
foreignKey: "contactId"
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
lookups: {
|
|
327
|
+
type: "object",
|
|
328
|
+
required: false
|
|
329
|
+
}
|
|
330
|
+
}, "replace");
|
|
331
|
+
const body = createSchemaDefinition({
|
|
332
|
+
name: {
|
|
333
|
+
type: "string",
|
|
334
|
+
required: true
|
|
335
|
+
}
|
|
336
|
+
}, "create");
|
|
337
|
+
const contracts = createCrudJsonApiRouteContracts({
|
|
338
|
+
resource: {
|
|
339
|
+
namespace: "contacts",
|
|
340
|
+
contract: {
|
|
341
|
+
lookup: {
|
|
342
|
+
containerKey: "lookups"
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
operations: {
|
|
346
|
+
view: { output },
|
|
347
|
+
create: { body, output },
|
|
348
|
+
patch: { body, output }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const responseDocument = contracts.viewRouteContract.transport.response(returnJsonApiData({
|
|
354
|
+
id: "contact-1",
|
|
355
|
+
name: "Alice",
|
|
356
|
+
lookups: {
|
|
357
|
+
pets: [
|
|
358
|
+
{
|
|
359
|
+
id: "pet-1",
|
|
360
|
+
name: "Ada",
|
|
361
|
+
contactId: "contact-1"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "pet-2",
|
|
365
|
+
name: "Bert",
|
|
366
|
+
contactId: "contact-1"
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
371
|
+
const resourceSchema = contracts.viewRouteContract.responses[200].transportSchema.definitions.contactsSuccessResource;
|
|
372
|
+
const relationshipDataSchema = resourceSchema.properties.relationships.properties.pets.properties.data;
|
|
373
|
+
|
|
374
|
+
assert.deepEqual(responseDocument.data.attributes, {
|
|
375
|
+
name: "Alice"
|
|
376
|
+
});
|
|
377
|
+
assert.deepEqual(responseDocument.data.relationships, {
|
|
378
|
+
pets: {
|
|
379
|
+
data: [
|
|
380
|
+
{
|
|
381
|
+
type: "pets",
|
|
382
|
+
id: "pet-1"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "pets",
|
|
386
|
+
id: "pet-2"
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
assert.deepEqual(responseDocument.included, [
|
|
392
|
+
{
|
|
393
|
+
type: "pets",
|
|
394
|
+
id: "pet-1",
|
|
395
|
+
attributes: {
|
|
396
|
+
name: "Ada",
|
|
397
|
+
contactId: "contact-1"
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
type: "pets",
|
|
402
|
+
id: "pet-2",
|
|
403
|
+
attributes: {
|
|
404
|
+
name: "Bert",
|
|
405
|
+
contactId: "contact-1"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
]);
|
|
409
|
+
assert.equal(relationshipDataSchema.anyOf[0].type, "array");
|
|
410
|
+
assert.equal(relationshipDataSchema.anyOf[0].items.properties.type.const, "pets");
|
|
411
|
+
});
|
|
412
|
+
|
|
287
413
|
test("createCrudJsonApiRouteContracts falls back to recordId params when no route params validator is provided", () => {
|
|
288
414
|
const contracts = createCrudJsonApiRouteContracts({
|
|
289
415
|
resource: createCrudResource()
|