@jskit-ai/crud-core 0.1.26 → 0.1.28
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 +17 -7
- package/src/client/composables/crudClientSupportHelpers.js +5 -17
- package/src/server/createCrudRepositoryFromResource.js +57 -0
- package/src/server/createCrudServiceFromResource.js +101 -0
- package/src/server/crudModuleConfig.js +13 -21
- package/src/server/fieldAccess.js +316 -0
- package/src/server/listQueryValidators.js +87 -0
- package/src/server/lookupHydration.js +546 -0
- package/src/server/lookupPathSupport.js +45 -0
- package/src/server/lookupProviders.js +43 -0
- package/src/server/repositoryMethods.js +381 -0
- package/src/server/repositorySupport.js +205 -0
- package/src/server/serviceEvents.js +53 -0
- package/src/shared/crudFieldMetaSupport.js +54 -0
- package/src/shared/crudNamespaceSupport.js +31 -0
- package/test/createCrudRepositoryFromResource.test.js +731 -0
- package/test/createCrudServiceFromResource.test.js +263 -0
- package/test/crudFieldMetaSupport.test.js +47 -0
- package/test/fieldAccess.test.js +86 -0
- package/test/listQueryValidators.test.js +162 -0
- package/test/lookupProviders.test.js +103 -0
- package/test/repositorySupport.test.js +282 -1
- package/test/serviceEvents.test.js +28 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createCrudRepositoryFromResource } from "../src/server/createCrudRepositoryFromResource.js";
|
|
4
|
+
|
|
5
|
+
function createListKnexDouble(rows = []) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
let firstMode = false;
|
|
8
|
+
const whereGroup = {
|
|
9
|
+
where(...args) {
|
|
10
|
+
calls.push(["where", ...args]);
|
|
11
|
+
return whereGroup;
|
|
12
|
+
},
|
|
13
|
+
orWhere(...args) {
|
|
14
|
+
calls.push(["orWhere", ...args]);
|
|
15
|
+
return whereGroup;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const query = {
|
|
20
|
+
select(...args) {
|
|
21
|
+
calls.push(["select", ...args]);
|
|
22
|
+
return query;
|
|
23
|
+
},
|
|
24
|
+
where(...args) {
|
|
25
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
26
|
+
calls.push(["whereCallback"]);
|
|
27
|
+
args[0](whereGroup);
|
|
28
|
+
return query;
|
|
29
|
+
}
|
|
30
|
+
calls.push(["where", ...args]);
|
|
31
|
+
return query;
|
|
32
|
+
},
|
|
33
|
+
orderBy(...args) {
|
|
34
|
+
calls.push(["orderBy", ...args]);
|
|
35
|
+
return query;
|
|
36
|
+
},
|
|
37
|
+
limit(value) {
|
|
38
|
+
calls.push(["limit", value]);
|
|
39
|
+
return query;
|
|
40
|
+
},
|
|
41
|
+
modify(callback) {
|
|
42
|
+
calls.push(["modify"]);
|
|
43
|
+
callback(query);
|
|
44
|
+
return query;
|
|
45
|
+
},
|
|
46
|
+
whereIn(...args) {
|
|
47
|
+
calls.push(["whereIn", ...args]);
|
|
48
|
+
return query;
|
|
49
|
+
},
|
|
50
|
+
first() {
|
|
51
|
+
calls.push(["first"]);
|
|
52
|
+
firstMode = true;
|
|
53
|
+
return query;
|
|
54
|
+
},
|
|
55
|
+
then(resolve, reject) {
|
|
56
|
+
const payload = firstMode ? rows[0] || null : rows;
|
|
57
|
+
return Promise.resolve(payload).then(resolve, reject);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const knex = (tableName) => {
|
|
62
|
+
calls.push(["table", tableName]);
|
|
63
|
+
return query;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
knex,
|
|
68
|
+
calls
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createResourceFixture() {
|
|
73
|
+
return {
|
|
74
|
+
resource: "contacts",
|
|
75
|
+
tableName: "contacts_table",
|
|
76
|
+
idColumn: "contact_id",
|
|
77
|
+
operations: {
|
|
78
|
+
view: {
|
|
79
|
+
outputValidator: {
|
|
80
|
+
schema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
id: { type: "integer" },
|
|
84
|
+
firstName: { type: "string" }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
create: {
|
|
90
|
+
bodyValidator: {
|
|
91
|
+
schema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
firstName: { type: "string" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
fieldMeta: [
|
|
101
|
+
{
|
|
102
|
+
key: "id",
|
|
103
|
+
dbColumn: "contact_id"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createLookupResourceFixture() {
|
|
110
|
+
return {
|
|
111
|
+
resource: "contacts",
|
|
112
|
+
tableName: "contacts_table",
|
|
113
|
+
idColumn: "contact_id",
|
|
114
|
+
operations: {
|
|
115
|
+
view: {
|
|
116
|
+
outputValidator: {
|
|
117
|
+
schema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
id: { type: "integer" },
|
|
121
|
+
firstName: { type: "string" },
|
|
122
|
+
primaryVetId: { type: "integer" },
|
|
123
|
+
secondaryVetId: { type: ["integer", "null"] },
|
|
124
|
+
lookups: {
|
|
125
|
+
type: "object"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
create: {
|
|
132
|
+
bodyValidator: {
|
|
133
|
+
schema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
firstName: { type: "string" },
|
|
137
|
+
primaryVetId: { type: "integer" },
|
|
138
|
+
secondaryVetId: { type: "integer" }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
fieldMeta: [
|
|
145
|
+
{
|
|
146
|
+
key: "id",
|
|
147
|
+
dbColumn: "contact_id"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "primaryVetId",
|
|
151
|
+
dbColumn: "primary_vet_id",
|
|
152
|
+
relation: {
|
|
153
|
+
kind: "lookup",
|
|
154
|
+
namespace: "vets",
|
|
155
|
+
valueKey: "id"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: "secondaryVetId",
|
|
160
|
+
dbColumn: "secondary_vet_id",
|
|
161
|
+
relation: {
|
|
162
|
+
kind: "lookup",
|
|
163
|
+
namespace: "vets",
|
|
164
|
+
valueKey: "id"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createLookupResourceWithCustomContainerKeyFixture() {
|
|
172
|
+
return {
|
|
173
|
+
resource: "contacts",
|
|
174
|
+
tableName: "contacts_table",
|
|
175
|
+
idColumn: "contact_id",
|
|
176
|
+
contract: {
|
|
177
|
+
lookup: {
|
|
178
|
+
containerKey: "lookupData"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
operations: {
|
|
182
|
+
view: {
|
|
183
|
+
outputValidator: {
|
|
184
|
+
schema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
id: { type: "integer" },
|
|
188
|
+
firstName: { type: "string" },
|
|
189
|
+
primaryVetId: { type: "integer" },
|
|
190
|
+
secondaryVetId: { type: "integer" },
|
|
191
|
+
lookupData: {
|
|
192
|
+
type: "object"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
create: {
|
|
199
|
+
bodyValidator: {
|
|
200
|
+
schema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
firstName: { type: "string" },
|
|
204
|
+
primaryVetId: { type: "integer" },
|
|
205
|
+
secondaryVetId: { type: "integer" }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
fieldMeta: [
|
|
212
|
+
{
|
|
213
|
+
key: "id",
|
|
214
|
+
dbColumn: "contact_id"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: "primaryVetId",
|
|
218
|
+
dbColumn: "primary_vet_id",
|
|
219
|
+
relation: {
|
|
220
|
+
kind: "lookup",
|
|
221
|
+
namespace: "vets",
|
|
222
|
+
valueKey: "id"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: "secondaryVetId",
|
|
227
|
+
dbColumn: "secondary_vet_id",
|
|
228
|
+
relation: {
|
|
229
|
+
kind: "lookup",
|
|
230
|
+
namespace: "vets",
|
|
231
|
+
valueKey: "id"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function createNormalizedResourceFixture() {
|
|
239
|
+
return {
|
|
240
|
+
resource: "contacts",
|
|
241
|
+
tableName: "contacts_table",
|
|
242
|
+
idColumn: "contact_id",
|
|
243
|
+
operations: {
|
|
244
|
+
view: {
|
|
245
|
+
outputValidator: {
|
|
246
|
+
schema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
id: { type: "integer" },
|
|
250
|
+
firstName: { type: "string" }
|
|
251
|
+
},
|
|
252
|
+
required: ["id", "firstName"]
|
|
253
|
+
},
|
|
254
|
+
normalize(payload = {}) {
|
|
255
|
+
return {
|
|
256
|
+
id: Number(payload.id),
|
|
257
|
+
firstName: String(payload.firstName || "").trim()
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
create: {
|
|
263
|
+
bodyValidator: {
|
|
264
|
+
schema: {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
firstName: { type: "string" }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
fieldMeta: [
|
|
274
|
+
{
|
|
275
|
+
key: "id",
|
|
276
|
+
dbColumn: "contact_id"
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
test("createCrudRepositoryFromResource requires table metadata from resource", () => {
|
|
283
|
+
assert.throws(
|
|
284
|
+
() =>
|
|
285
|
+
createCrudRepositoryFromResource({
|
|
286
|
+
operations: {
|
|
287
|
+
view: {
|
|
288
|
+
outputValidator: {
|
|
289
|
+
schema: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
id: { type: "integer" }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
create: {
|
|
298
|
+
bodyValidator: {
|
|
299
|
+
schema: {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
fieldMeta: []
|
|
307
|
+
}),
|
|
308
|
+
/requires resource\.tableName or resource\.resource/
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("createCrudRepositoryFromResource defaults table and id columns from resource", async () => {
|
|
313
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
314
|
+
const { knex, calls } = createListKnexDouble([
|
|
315
|
+
{
|
|
316
|
+
contact_id: 3,
|
|
317
|
+
first_name: "Tony"
|
|
318
|
+
}
|
|
319
|
+
]);
|
|
320
|
+
const repository = createRepository(knex);
|
|
321
|
+
|
|
322
|
+
const result = await repository.list({
|
|
323
|
+
cursor: 2,
|
|
324
|
+
q: "to"
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
assert.deepEqual(result, {
|
|
328
|
+
items: [
|
|
329
|
+
{
|
|
330
|
+
id: 3,
|
|
331
|
+
firstName: "Tony"
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
nextCursor: null
|
|
335
|
+
});
|
|
336
|
+
assert.equal(calls[0][1], "contacts_table");
|
|
337
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === ">" && call[3] === 2));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("createCrudRepositoryFromResource createRepository requires knex", () => {
|
|
341
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
342
|
+
assert.throws(
|
|
343
|
+
() => createRepository(null),
|
|
344
|
+
/requires knex/
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("createCrudRepositoryFromResource allows list tuning through list config", async () => {
|
|
349
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture(), {
|
|
350
|
+
list: {
|
|
351
|
+
defaultLimit: 1,
|
|
352
|
+
maxLimit: 2,
|
|
353
|
+
searchColumns: ["first_name"]
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
const { knex, calls } = createListKnexDouble([
|
|
357
|
+
{ contact_id: 3, first_name: "Tony" },
|
|
358
|
+
{ contact_id: 4, first_name: "Tom" },
|
|
359
|
+
{ contact_id: 5, first_name: "Toby" }
|
|
360
|
+
]);
|
|
361
|
+
const repository = createRepository(knex);
|
|
362
|
+
|
|
363
|
+
await repository.list({
|
|
364
|
+
q: "to",
|
|
365
|
+
limit: 99
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "first_name" && call[2] === "like" && call[3] === "%to%"));
|
|
369
|
+
assert.ok(calls.some((call) => call[0] === "limit" && call[1] === 3));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("createCrudRepositoryFromResource exposes listByIds for lookup providers", async () => {
|
|
373
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
374
|
+
const { knex, calls } = createListKnexDouble([
|
|
375
|
+
{
|
|
376
|
+
contact_id: 3,
|
|
377
|
+
first_name: "Tony"
|
|
378
|
+
}
|
|
379
|
+
]);
|
|
380
|
+
const repository = createRepository(knex);
|
|
381
|
+
|
|
382
|
+
const records = await repository.listByIds([3, 3, 4]);
|
|
383
|
+
|
|
384
|
+
assert.equal(records.length, 1);
|
|
385
|
+
assert.deepEqual(records[0], {
|
|
386
|
+
id: 3,
|
|
387
|
+
firstName: "Tony"
|
|
388
|
+
});
|
|
389
|
+
assert.ok(calls.some((call) => call[0] === "whereIn" && call[1] === "contact_id"));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("createCrudRepositoryFromResource listByIds fails fast when valueKey is not in output schema", async () => {
|
|
393
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
394
|
+
const { knex } = createListKnexDouble([
|
|
395
|
+
{
|
|
396
|
+
contact_id: 3,
|
|
397
|
+
first_name: "Tony"
|
|
398
|
+
}
|
|
399
|
+
]);
|
|
400
|
+
const repository = createRepository(knex);
|
|
401
|
+
|
|
402
|
+
await assert.rejects(
|
|
403
|
+
() =>
|
|
404
|
+
repository.listByIds([3], {
|
|
405
|
+
valueKey: "externalCustomerId"
|
|
406
|
+
}),
|
|
407
|
+
/valueKey "externalCustomerId" to exist in output schema/
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("createCrudRepositoryFromResource normalizes listByIds output using resource view output validator", async () => {
|
|
412
|
+
const createRepository = createCrudRepositoryFromResource(createNormalizedResourceFixture());
|
|
413
|
+
const { knex } = createListKnexDouble([
|
|
414
|
+
{
|
|
415
|
+
contact_id: "3",
|
|
416
|
+
first_name: " Tony "
|
|
417
|
+
}
|
|
418
|
+
]);
|
|
419
|
+
const repository = createRepository(knex);
|
|
420
|
+
|
|
421
|
+
const result = await repository.listByIds([3]);
|
|
422
|
+
assert.deepEqual(result, [
|
|
423
|
+
{
|
|
424
|
+
id: 3,
|
|
425
|
+
firstName: "Tony"
|
|
426
|
+
}
|
|
427
|
+
]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("createCrudRepositoryFromResource fails when mapped output violates resource view output schema", async () => {
|
|
431
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
432
|
+
const { knex } = createListKnexDouble([
|
|
433
|
+
{
|
|
434
|
+
contact_id: "3",
|
|
435
|
+
first_name: "Tony"
|
|
436
|
+
}
|
|
437
|
+
]);
|
|
438
|
+
const repository = createRepository(knex);
|
|
439
|
+
|
|
440
|
+
await assert.rejects(
|
|
441
|
+
() => repository.listByIds([3]),
|
|
442
|
+
/output validation failed/
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("createCrudRepositoryFromResource hydrates lookup relations by default and batches by lookup resource", async () => {
|
|
447
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceFixture());
|
|
448
|
+
const { knex } = createListKnexDouble([
|
|
449
|
+
{
|
|
450
|
+
contact_id: 3,
|
|
451
|
+
first_name: "Tony",
|
|
452
|
+
primary_vet_id: 10,
|
|
453
|
+
secondary_vet_id: 12
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
contact_id: 4,
|
|
457
|
+
first_name: "Sara",
|
|
458
|
+
primary_vet_id: 10,
|
|
459
|
+
secondary_vet_id: null
|
|
460
|
+
}
|
|
461
|
+
]);
|
|
462
|
+
|
|
463
|
+
const lookupCalls = [];
|
|
464
|
+
const repository = createRepository(knex, {
|
|
465
|
+
resolveLookupProvider(relation = {}) {
|
|
466
|
+
assert.equal(relation.namespace, "vets");
|
|
467
|
+
return {
|
|
468
|
+
async listByIds(ids = [], options = {}) {
|
|
469
|
+
lookupCalls.push({
|
|
470
|
+
ids,
|
|
471
|
+
options
|
|
472
|
+
});
|
|
473
|
+
return [
|
|
474
|
+
{ id: 10, name: "Vet A" },
|
|
475
|
+
{ id: 12, name: "Vet B" }
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const result = await repository.list({});
|
|
483
|
+
|
|
484
|
+
assert.equal(lookupCalls.length, 1);
|
|
485
|
+
assert.deepEqual(lookupCalls[0].ids, [10, 12]);
|
|
486
|
+
assert.equal(lookupCalls[0].options.include, "*");
|
|
487
|
+
assert.equal(lookupCalls[0].options.lookupDepth, 1);
|
|
488
|
+
assert.equal(lookupCalls[0].options.lookupMaxDepth, 3);
|
|
489
|
+
assert.deepEqual(result.items[0].lookups, {
|
|
490
|
+
primaryVetId: { id: 10, name: "Vet A" },
|
|
491
|
+
secondaryVetId: { id: 12, name: "Vet B" }
|
|
492
|
+
});
|
|
493
|
+
assert.deepEqual(result.items[1].lookups, {
|
|
494
|
+
primaryVetId: { id: 10, name: "Vet A" },
|
|
495
|
+
secondaryVetId: null
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("createCrudRepositoryFromResource writes hydrated lookups into custom output container key", async () => {
|
|
500
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceWithCustomContainerKeyFixture());
|
|
501
|
+
const { knex } = createListKnexDouble([
|
|
502
|
+
{
|
|
503
|
+
contact_id: 3,
|
|
504
|
+
first_name: "Tony",
|
|
505
|
+
primary_vet_id: 10,
|
|
506
|
+
secondary_vet_id: 12
|
|
507
|
+
}
|
|
508
|
+
]);
|
|
509
|
+
const repository = createRepository(knex, {
|
|
510
|
+
resolveLookupProvider() {
|
|
511
|
+
return {
|
|
512
|
+
async listByIds() {
|
|
513
|
+
return [
|
|
514
|
+
{ id: 10, name: "Vet A" },
|
|
515
|
+
{ id: 12, name: "Vet B" }
|
|
516
|
+
];
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const result = await repository.list({});
|
|
523
|
+
assert.equal(Object.hasOwn(result.items[0], "lookups"), false);
|
|
524
|
+
assert.deepEqual(result.items[0].lookupData, {
|
|
525
|
+
primaryVetId: { id: 10, name: "Vet A" },
|
|
526
|
+
secondaryVetId: { id: 12, name: "Vet B" }
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("createCrudRepositoryFromResource respects resource lookup.defaultInclude=none", async () => {
|
|
531
|
+
const resource = createLookupResourceFixture();
|
|
532
|
+
resource.contract = {
|
|
533
|
+
lookup: {
|
|
534
|
+
defaultInclude: "none"
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const createRepository = createCrudRepositoryFromResource(resource);
|
|
538
|
+
const { knex } = createListKnexDouble([
|
|
539
|
+
{
|
|
540
|
+
contact_id: 3,
|
|
541
|
+
first_name: "Tony",
|
|
542
|
+
primary_vet_id: 10,
|
|
543
|
+
secondary_vet_id: 12
|
|
544
|
+
}
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
let resolverCalls = 0;
|
|
548
|
+
const repository = createRepository(knex, {
|
|
549
|
+
resolveLookupProvider() {
|
|
550
|
+
resolverCalls += 1;
|
|
551
|
+
return {
|
|
552
|
+
async listByIds() {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const result = await repository.list({});
|
|
560
|
+
assert.equal(resolverCalls, 0);
|
|
561
|
+
assert.equal(Object.hasOwn(result.items[0], "lookups"), false);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("createCrudRepositoryFromResource skips lookup hydration when include=none", async () => {
|
|
565
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceFixture());
|
|
566
|
+
const { knex } = createListKnexDouble([
|
|
567
|
+
{
|
|
568
|
+
contact_id: 3,
|
|
569
|
+
first_name: "Tony",
|
|
570
|
+
primary_vet_id: 10,
|
|
571
|
+
secondary_vet_id: 12
|
|
572
|
+
}
|
|
573
|
+
]);
|
|
574
|
+
const repository = createRepository(knex);
|
|
575
|
+
|
|
576
|
+
const result = await repository.list({
|
|
577
|
+
include: "none"
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
assert.equal(result.items.length, 1);
|
|
581
|
+
assert.equal(Object.hasOwn(result.items[0], "lookups"), false);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("createCrudRepositoryFromResource forwards nested include paths to child lookup repositories", async () => {
|
|
585
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceFixture());
|
|
586
|
+
const { knex } = createListKnexDouble([
|
|
587
|
+
{
|
|
588
|
+
contact_id: 3,
|
|
589
|
+
first_name: "Tony",
|
|
590
|
+
primary_vet_id: 10,
|
|
591
|
+
secondary_vet_id: 12
|
|
592
|
+
}
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
const lookupCalls = [];
|
|
596
|
+
const repository = createRepository(knex, {
|
|
597
|
+
resolveLookupProvider() {
|
|
598
|
+
return {
|
|
599
|
+
async listByIds(ids = [], options = {}) {
|
|
600
|
+
lookupCalls.push({
|
|
601
|
+
ids,
|
|
602
|
+
options
|
|
603
|
+
});
|
|
604
|
+
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
await repository.list({
|
|
611
|
+
include: "primaryVetId.vetTypeId"
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
assert.equal(lookupCalls.length, 1);
|
|
615
|
+
assert.equal(lookupCalls[0].options.include, "vetTypeId");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("createCrudRepositoryFromResource forwards wildcard nested include paths to child lookup repositories", async () => {
|
|
619
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceFixture());
|
|
620
|
+
const { knex } = createListKnexDouble([
|
|
621
|
+
{
|
|
622
|
+
contact_id: 3,
|
|
623
|
+
first_name: "Tony",
|
|
624
|
+
primary_vet_id: 10,
|
|
625
|
+
secondary_vet_id: 12
|
|
626
|
+
}
|
|
627
|
+
]);
|
|
628
|
+
|
|
629
|
+
const lookupCalls = [];
|
|
630
|
+
const repository = createRepository(knex, {
|
|
631
|
+
resolveLookupProvider() {
|
|
632
|
+
return {
|
|
633
|
+
async listByIds(ids = [], options = {}) {
|
|
634
|
+
lookupCalls.push({
|
|
635
|
+
ids,
|
|
636
|
+
options
|
|
637
|
+
});
|
|
638
|
+
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
await repository.list({
|
|
645
|
+
include: "primaryVetId.*"
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
assert.equal(lookupCalls.length, 1);
|
|
649
|
+
assert.equal(lookupCalls[0].options.include, "*");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("createCrudRepositoryFromResource forwards configured lookup maxDepth to child repositories", async () => {
|
|
653
|
+
const resource = createLookupResourceFixture();
|
|
654
|
+
resource.contract = {
|
|
655
|
+
lookup: {
|
|
656
|
+
maxDepth: 5
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
const createRepository = createCrudRepositoryFromResource(resource);
|
|
660
|
+
const { knex } = createListKnexDouble([
|
|
661
|
+
{
|
|
662
|
+
contact_id: 3,
|
|
663
|
+
first_name: "Tony",
|
|
664
|
+
primary_vet_id: 10,
|
|
665
|
+
secondary_vet_id: 12
|
|
666
|
+
}
|
|
667
|
+
]);
|
|
668
|
+
|
|
669
|
+
const lookupCalls = [];
|
|
670
|
+
const repository = createRepository(knex, {
|
|
671
|
+
resolveLookupProvider() {
|
|
672
|
+
return {
|
|
673
|
+
async listByIds(ids = [], options = {}) {
|
|
674
|
+
lookupCalls.push({
|
|
675
|
+
ids,
|
|
676
|
+
options
|
|
677
|
+
});
|
|
678
|
+
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
await repository.list({});
|
|
685
|
+
assert.equal(lookupCalls.length, 1);
|
|
686
|
+
assert.equal(lookupCalls[0].options.lookupMaxDepth, 5);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("createCrudRepositoryFromResource throws when include requires lookups and resolver is missing", async () => {
|
|
690
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceFixture());
|
|
691
|
+
const { knex } = createListKnexDouble([
|
|
692
|
+
{
|
|
693
|
+
contact_id: 3,
|
|
694
|
+
first_name: "Tony",
|
|
695
|
+
primary_vet_id: 10,
|
|
696
|
+
secondary_vet_id: 12
|
|
697
|
+
}
|
|
698
|
+
]);
|
|
699
|
+
const repository = createRepository(knex);
|
|
700
|
+
|
|
701
|
+
await assert.rejects(
|
|
702
|
+
() => repository.list({}),
|
|
703
|
+
/requires resolveLookupProvider/
|
|
704
|
+
);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("createCrudRepositoryFromResource throws when include references unknown lookup field", async () => {
|
|
708
|
+
const createRepository = createCrudRepositoryFromResource(createLookupResourceFixture());
|
|
709
|
+
const { knex } = createListKnexDouble([
|
|
710
|
+
{
|
|
711
|
+
contact_id: 3,
|
|
712
|
+
first_name: "Tony",
|
|
713
|
+
primary_vet_id: 10,
|
|
714
|
+
secondary_vet_id: 12
|
|
715
|
+
}
|
|
716
|
+
]);
|
|
717
|
+
const repository = createRepository(knex, {
|
|
718
|
+
resolveLookupProvider() {
|
|
719
|
+
return {
|
|
720
|
+
async listByIds() {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
await assert.rejects(
|
|
728
|
+
() => repository.list({ include: "unknownLookupKey" }),
|
|
729
|
+
/unknown lookup key/
|
|
730
|
+
);
|
|
731
|
+
});
|