@jskit-ai/crud-core 0.1.30 → 0.1.31
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 +7 -6
- package/src/server/createCrudRepositoryFromResource.js +18 -12
- package/src/server/createHooksToCollectChildren.js +247 -0
- package/src/server/lookupHydration.js +172 -11
- package/src/server/lookupProviders.js +33 -1
- package/src/server/repositoryMethods.js +808 -58
- package/test/createCrudRepositoryFromResource.test.js +1091 -71
- package/test/createHooksToCollectChildren.test.js +251 -0
- package/test/lookupProviders.test.js +32 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createHooksToCollectChildren } from "../src/server/createHooksToCollectChildren.js";
|
|
4
|
+
|
|
5
|
+
test("createHooksToCollectChildren batches children and hydrates returned records", async () => {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const hooks = createHooksToCollectChildren({
|
|
8
|
+
childKey: "pets",
|
|
9
|
+
childOwnerIdKey: "contactId",
|
|
10
|
+
async listChildren(contactIds = [], options = {}) {
|
|
11
|
+
calls.push({
|
|
12
|
+
contactIds,
|
|
13
|
+
options
|
|
14
|
+
});
|
|
15
|
+
return [
|
|
16
|
+
{ id: 100, contactId: 7, name: "Milo" },
|
|
17
|
+
{ id: 101, contactId: 7, name: "Luna" },
|
|
18
|
+
{ id: 200, contactId: 8, name: "Mochi" }
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const context = {
|
|
24
|
+
callOptions: {
|
|
25
|
+
trx: { id: "trx-1" },
|
|
26
|
+
visibilityContext: { visibility: "workspace", scopeOwnerId: "workspace-1" },
|
|
27
|
+
ignored: true
|
|
28
|
+
},
|
|
29
|
+
state: {}
|
|
30
|
+
};
|
|
31
|
+
const records = [{ id: 7, firstName: "Tony" }, { id: 8, firstName: "Sara" }, { id: 7, firstName: "Tony2" }];
|
|
32
|
+
|
|
33
|
+
await hooks.afterQuery(records, context);
|
|
34
|
+
const hydrated = records.map((record) => hooks.transformReturnedRecord(record, context));
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(calls, [
|
|
37
|
+
{
|
|
38
|
+
contactIds: [7, 8],
|
|
39
|
+
options: {
|
|
40
|
+
trx: context.callOptions.trx,
|
|
41
|
+
visibilityContext: context.callOptions.visibilityContext
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
45
|
+
assert.deepEqual(hydrated[0].lookups?.pets?.map((entry) => entry.name), ["Milo", "Luna"]);
|
|
46
|
+
assert.deepEqual(hydrated[1].lookups?.pets?.map((entry) => entry.name), ["Mochi"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("createHooksToCollectChildren supports childRepository + childListMethod shorthand", async () => {
|
|
50
|
+
const calls = [];
|
|
51
|
+
const childRepository = {
|
|
52
|
+
async listByContactIds(contactIds = [], options = {}) {
|
|
53
|
+
calls.push({
|
|
54
|
+
contactIds,
|
|
55
|
+
options
|
|
56
|
+
});
|
|
57
|
+
return [{ id: 1, contactId: 9 }];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const hooks = createHooksToCollectChildren({
|
|
61
|
+
childKey: "pets",
|
|
62
|
+
childOwnerIdKey: "contactId",
|
|
63
|
+
childRepository,
|
|
64
|
+
childListMethod: "listByContactIds"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const context = {
|
|
68
|
+
callOptions: {
|
|
69
|
+
trx: { id: "trx-2" }
|
|
70
|
+
},
|
|
71
|
+
state: {}
|
|
72
|
+
};
|
|
73
|
+
await hooks.afterQuery([{ id: 9 }], context);
|
|
74
|
+
const hydrated = hooks.transformReturnedRecord({ id: 9 }, context);
|
|
75
|
+
|
|
76
|
+
assert.equal(calls.length, 1);
|
|
77
|
+
assert.deepEqual(calls[0].contactIds, [9]);
|
|
78
|
+
assert.deepEqual(calls[0].options, {
|
|
79
|
+
trx: context.callOptions.trx
|
|
80
|
+
});
|
|
81
|
+
assert.equal(hydrated.lookups?.pets?.length, 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("createHooksToCollectChildren uses childRepository.listByIds by default", async () => {
|
|
85
|
+
const calls = [];
|
|
86
|
+
const childRepository = {
|
|
87
|
+
async listByIds(contactIds = [], options = {}) {
|
|
88
|
+
calls.push({
|
|
89
|
+
contactIds,
|
|
90
|
+
options
|
|
91
|
+
});
|
|
92
|
+
return [{ id: 11, customerId: 9, name: "Milo" }];
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const hooks = createHooksToCollectChildren({
|
|
96
|
+
childKey: "pets",
|
|
97
|
+
childRepository,
|
|
98
|
+
childForeignKey: "customerId"
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const context = {
|
|
102
|
+
callOptions: {
|
|
103
|
+
trx: { id: "trx-4" }
|
|
104
|
+
},
|
|
105
|
+
state: {}
|
|
106
|
+
};
|
|
107
|
+
await hooks.afterQuery([{ id: 9 }], context);
|
|
108
|
+
const hydrated = hooks.transformReturnedRecord({ id: 9 }, context);
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(calls, [
|
|
111
|
+
{
|
|
112
|
+
contactIds: [9],
|
|
113
|
+
options: {
|
|
114
|
+
trx: context.callOptions.trx,
|
|
115
|
+
valueKey: "customerId"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]);
|
|
119
|
+
assert.equal(hydrated.lookups?.pets?.length, 1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("createHooksToCollectChildren supports override hooks for parent id, child owner id, call options, and attach", async () => {
|
|
123
|
+
const calls = [];
|
|
124
|
+
const hooks = createHooksToCollectChildren({
|
|
125
|
+
childKey: "pets",
|
|
126
|
+
listChildren(contactIds = [], options = {}) {
|
|
127
|
+
calls.push({
|
|
128
|
+
contactIds,
|
|
129
|
+
options
|
|
130
|
+
});
|
|
131
|
+
return [{ id: "p-1", ownerId: "contact-15" }];
|
|
132
|
+
},
|
|
133
|
+
getParentId(record = {}) {
|
|
134
|
+
return `contact-${record.contactId}`;
|
|
135
|
+
},
|
|
136
|
+
getChildOwnerId(child = {}) {
|
|
137
|
+
return child.ownerId;
|
|
138
|
+
},
|
|
139
|
+
buildChildCallOptions({ callOptions = {}, ownerIds = [] } = {}) {
|
|
140
|
+
return {
|
|
141
|
+
trx: callOptions.trx || null,
|
|
142
|
+
includeArchived: ownerIds.length > 0
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
attachChildren(record = {}, children = []) {
|
|
146
|
+
return {
|
|
147
|
+
...record,
|
|
148
|
+
petCount: children.length
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const context = {
|
|
154
|
+
callOptions: {
|
|
155
|
+
trx: { id: "trx-3" }
|
|
156
|
+
},
|
|
157
|
+
state: {}
|
|
158
|
+
};
|
|
159
|
+
await hooks.afterQuery([{ contactId: 15 }], context);
|
|
160
|
+
const hydrated = hooks.transformReturnedRecord({ contactId: 15 }, context);
|
|
161
|
+
|
|
162
|
+
assert.deepEqual(calls, [
|
|
163
|
+
{
|
|
164
|
+
contactIds: ["contact-15"],
|
|
165
|
+
options: {
|
|
166
|
+
trx: context.callOptions.trx,
|
|
167
|
+
includeArchived: true
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
]);
|
|
171
|
+
assert.deepEqual(hydrated, {
|
|
172
|
+
contactId: 15,
|
|
173
|
+
petCount: 1
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("createHooksToCollectChildren validates required configuration", async () => {
|
|
178
|
+
assert.throws(
|
|
179
|
+
() =>
|
|
180
|
+
createHooksToCollectChildren({
|
|
181
|
+
childOwnerIdKey: "contactId",
|
|
182
|
+
async listChildren() {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
/requires childKey/
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
assert.throws(
|
|
190
|
+
() =>
|
|
191
|
+
createHooksToCollectChildren({
|
|
192
|
+
childKey: "pets",
|
|
193
|
+
async listChildren() {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}),
|
|
197
|
+
/requires childOwnerIdKey, childForeignKey, or getChildOwnerId/
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
assert.throws(
|
|
201
|
+
() =>
|
|
202
|
+
createHooksToCollectChildren({
|
|
203
|
+
childKey: "pets"
|
|
204
|
+
}),
|
|
205
|
+
/requires listChildren\(ids, options, ctx\) or childRepository/
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
assert.throws(
|
|
209
|
+
() =>
|
|
210
|
+
createHooksToCollectChildren({
|
|
211
|
+
childKey: "pets",
|
|
212
|
+
childRepository: {
|
|
213
|
+
async listByIds() {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
/requires childForeignKey when using childRepository\.listByIds/
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const hooks = createHooksToCollectChildren({
|
|
222
|
+
childKey: "pets",
|
|
223
|
+
childOwnerIdKey: "contactId",
|
|
224
|
+
async listChildren() {
|
|
225
|
+
return "not-an-array";
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
await assert.rejects(
|
|
229
|
+
() =>
|
|
230
|
+
hooks.afterQuery([{ id: 7 }], {
|
|
231
|
+
state: {},
|
|
232
|
+
callOptions: {}
|
|
233
|
+
}),
|
|
234
|
+
/listChildren must return an array/
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("createHooksToCollectChildren requires ctx.state during afterQuery", async () => {
|
|
239
|
+
const hooks = createHooksToCollectChildren({
|
|
240
|
+
childKey: "pets",
|
|
241
|
+
childOwnerIdKey: "contactId",
|
|
242
|
+
async listChildren() {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await assert.rejects(
|
|
248
|
+
() => hooks.afterQuery([{ id: 1 }], {}),
|
|
249
|
+
/requires ctx\.state object/
|
|
250
|
+
);
|
|
251
|
+
});
|
|
@@ -46,6 +46,7 @@ test("createCrudLookupProviderResolver resolves providers through scope.make()",
|
|
|
46
46
|
test("createCrudLookupProvider wraps repository.listByIds and preserves include when provided", async () => {
|
|
47
47
|
const calls = [];
|
|
48
48
|
const provider = createCrudLookupProvider({
|
|
49
|
+
ownershipFilter: "workspace",
|
|
49
50
|
async listByIds(ids = [], options = {}) {
|
|
50
51
|
calls.push({
|
|
51
52
|
ids,
|
|
@@ -61,6 +62,7 @@ test("createCrudLookupProvider wraps repository.listByIds and preserves include
|
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
assert.deepEqual(result, [{ id: 1 }]);
|
|
65
|
+
assert.equal(provider.ownershipFilter, "workspace");
|
|
64
66
|
assert.deepEqual(calls[0], {
|
|
65
67
|
ids: [1, 2],
|
|
66
68
|
options: {
|
|
@@ -93,6 +95,36 @@ test("createCrudLookupProvider defaults include=none when include is not provide
|
|
|
93
95
|
limit: 10
|
|
94
96
|
}
|
|
95
97
|
});
|
|
98
|
+
assert.equal(provider.ownershipFilter, null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("createCrudLookupProvider accepts ownershipFilter option override", () => {
|
|
102
|
+
const provider = createCrudLookupProvider(
|
|
103
|
+
{
|
|
104
|
+
ownershipFilter: "workspace",
|
|
105
|
+
async listByIds() {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
ownershipFilter: "public"
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
assert.equal(provider.ownershipFilter, "public");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("createCrudLookupProvider validates ownershipFilter token", () => {
|
|
118
|
+
assert.throws(
|
|
119
|
+
() =>
|
|
120
|
+
createCrudLookupProvider({
|
|
121
|
+
ownershipFilter: "workspace-only",
|
|
122
|
+
async listByIds() {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
/must be one of/
|
|
127
|
+
);
|
|
96
128
|
});
|
|
97
129
|
|
|
98
130
|
test("createCrudLookupProvider validates repository contract", () => {
|