@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.
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.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.
|
|
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.
|
|
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.
|
|
32
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
33
|
-
"@jskit-ai/kernel": "0.1.
|
|
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.
|
|
36
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
37
|
-
"@jskit-ai/shell-web": "0.1.
|
|
38
|
-
"@jskit-ai/users-core": "0.1.
|
|
39
|
-
"@jskit-ai/users-web": "0.1.
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
53
|
-
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:
|
|
61
|
-
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", () => {
|