@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.
@@ -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", () => {