@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
|
@@ -6,10 +6,49 @@ import {
|
|
|
6
6
|
requireCrudTableName,
|
|
7
7
|
buildWritePayload,
|
|
8
8
|
mapRecordRow,
|
|
9
|
+
applyCrudListQueryFilters,
|
|
9
10
|
resolveCrudIdColumn,
|
|
10
|
-
buildRepositoryColumnMetadata
|
|
11
|
+
buildRepositoryColumnMetadata,
|
|
12
|
+
deriveRepositoryMappingFromResource
|
|
11
13
|
} from "../src/server/repositorySupport.js";
|
|
12
14
|
|
|
15
|
+
function createQueryDouble() {
|
|
16
|
+
const calls = [];
|
|
17
|
+
const whereQuery = {
|
|
18
|
+
where(...args) {
|
|
19
|
+
calls.push(["innerWhere", ...args]);
|
|
20
|
+
return whereQuery;
|
|
21
|
+
},
|
|
22
|
+
orWhere(...args) {
|
|
23
|
+
calls.push(["innerOrWhere", ...args]);
|
|
24
|
+
return whereQuery;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const query = {
|
|
29
|
+
modify(callback) {
|
|
30
|
+
calls.push(["modify"]);
|
|
31
|
+
callback(query);
|
|
32
|
+
return query;
|
|
33
|
+
},
|
|
34
|
+
where(...args) {
|
|
35
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
36
|
+
calls.push(["whereGroup"]);
|
|
37
|
+
args[0](whereQuery);
|
|
38
|
+
return query;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
calls.push(["where", ...args]);
|
|
42
|
+
return query;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
query,
|
|
48
|
+
calls
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
13
52
|
test("normalizeCrudListLimit enforces fallback and max", () => {
|
|
14
53
|
assert.equal(normalizeCrudListLimit(null), DEFAULT_LIST_LIMIT);
|
|
15
54
|
assert.equal(normalizeCrudListLimit("abc"), DEFAULT_LIST_LIMIT);
|
|
@@ -53,6 +92,62 @@ test("buildWritePayload respects defined keys", () => {
|
|
|
53
92
|
});
|
|
54
93
|
});
|
|
55
94
|
|
|
95
|
+
test("applyCrudListQueryFilters applies search and cursor filters", () => {
|
|
96
|
+
const { query, calls } = createQueryDouble();
|
|
97
|
+
const result = applyCrudListQueryFilters(query, {
|
|
98
|
+
idColumn: "id",
|
|
99
|
+
cursor: "3",
|
|
100
|
+
q: "ani",
|
|
101
|
+
searchColumns: ["first_name", "last_name"]
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.equal(result, query);
|
|
105
|
+
assert.deepEqual(calls, [
|
|
106
|
+
["modify"],
|
|
107
|
+
["whereGroup"],
|
|
108
|
+
["innerWhere", "first_name", "like", "%ani%"],
|
|
109
|
+
["innerOrWhere", "last_name", "like", "%ani%"],
|
|
110
|
+
["where", "id", ">", 3]
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("applyCrudListQueryFilters skips search and cursor when inputs are empty", () => {
|
|
115
|
+
const { query, calls } = createQueryDouble();
|
|
116
|
+
const result = applyCrudListQueryFilters(query, {
|
|
117
|
+
cursor: 0,
|
|
118
|
+
q: " ",
|
|
119
|
+
searchColumns: []
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.equal(result, query);
|
|
123
|
+
assert.deepEqual(calls, []);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("applyCrudListQueryFilters applies parent FK filters from allowed columns", () => {
|
|
127
|
+
const { query, calls } = createQueryDouble();
|
|
128
|
+
const result = applyCrudListQueryFilters(query, {
|
|
129
|
+
parentFilters: {
|
|
130
|
+
contactId: " 7 ",
|
|
131
|
+
ignored: "x"
|
|
132
|
+
},
|
|
133
|
+
parentFilterColumns: {
|
|
134
|
+
contactId: "contact_id"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(result, query);
|
|
139
|
+
assert.deepEqual(calls, [
|
|
140
|
+
["where", "contact_id", "7"]
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("applyCrudListQueryFilters throws for invalid query builders", () => {
|
|
145
|
+
assert.throws(
|
|
146
|
+
() => applyCrudListQueryFilters({}),
|
|
147
|
+
/requires query builder/
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
56
151
|
test("resolveCrudIdColumn falls back and rejects empties", () => {
|
|
57
152
|
assert.equal(resolveCrudIdColumn(" custom_id "), "custom_id");
|
|
58
153
|
assert.equal(resolveCrudIdColumn(undefined, { fallback: "fallback_id" }), "fallback_id");
|
|
@@ -71,3 +166,189 @@ test("buildRepositoryColumnMetadata normalizes columns and applies overrides", (
|
|
|
71
166
|
assert.equal(metadata.writeMappings.length, 1);
|
|
72
167
|
assert.equal(metadata.outputMappings[1].column, "surname");
|
|
73
168
|
});
|
|
169
|
+
|
|
170
|
+
test("deriveRepositoryMappingFromResource reads schema keys and fieldMeta dbColumn overrides", () => {
|
|
171
|
+
const resource = {
|
|
172
|
+
operations: {
|
|
173
|
+
view: {
|
|
174
|
+
outputValidator: {
|
|
175
|
+
schema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
id: { type: "integer" },
|
|
179
|
+
firstName: { type: "string" },
|
|
180
|
+
createdAt: { type: "string" }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
create: {
|
|
186
|
+
bodyValidator: {
|
|
187
|
+
schema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
firstName: { type: "string" },
|
|
191
|
+
vetId: { type: "integer" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
fieldMeta: [
|
|
198
|
+
{
|
|
199
|
+
key: "createdAt",
|
|
200
|
+
dbColumn: "created_at"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
key: "vetId",
|
|
204
|
+
dbColumn: "vet_id",
|
|
205
|
+
relation: {
|
|
206
|
+
kind: "lookup",
|
|
207
|
+
namespace: "vets",
|
|
208
|
+
valueKey: "id"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const mapping = deriveRepositoryMappingFromResource(resource);
|
|
215
|
+
assert.deepEqual(mapping.outputKeys, ["id", "firstName", "createdAt"]);
|
|
216
|
+
assert.deepEqual(mapping.writeKeys, ["firstName", "vetId"]);
|
|
217
|
+
assert.deepEqual(mapping.columnOverrides, {
|
|
218
|
+
createdAt: "created_at",
|
|
219
|
+
vetId: "vet_id"
|
|
220
|
+
});
|
|
221
|
+
assert.deepEqual(mapping.listSearchColumns, ["first_name", "created_at"]);
|
|
222
|
+
assert.deepEqual(mapping.parentFilterColumns, {
|
|
223
|
+
vetId: "vet_id"
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("deriveRepositoryMappingFromResource excludes runtime-only lookups output key from db mapping", () => {
|
|
228
|
+
const resource = {
|
|
229
|
+
operations: {
|
|
230
|
+
view: {
|
|
231
|
+
outputValidator: {
|
|
232
|
+
schema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
id: { type: "integer" },
|
|
236
|
+
firstName: { type: "string" },
|
|
237
|
+
lookups: { type: "object" }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
create: {
|
|
243
|
+
bodyValidator: {
|
|
244
|
+
schema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {
|
|
247
|
+
firstName: { type: "string" }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
fieldMeta: []
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const mapping = deriveRepositoryMappingFromResource(resource);
|
|
257
|
+
assert.deepEqual(mapping.outputKeys, ["id", "firstName"]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("deriveRepositoryMappingFromResource excludes custom lookup output container key", () => {
|
|
261
|
+
const resource = {
|
|
262
|
+
contract: {
|
|
263
|
+
lookup: {
|
|
264
|
+
containerKey: "lookupData"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
operations: {
|
|
268
|
+
view: {
|
|
269
|
+
outputValidator: {
|
|
270
|
+
schema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
properties: {
|
|
273
|
+
id: { type: "integer" },
|
|
274
|
+
firstName: { type: "string" },
|
|
275
|
+
lookupData: { type: "object" }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
create: {
|
|
281
|
+
bodyValidator: {
|
|
282
|
+
schema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
firstName: { type: "string" }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
fieldMeta: []
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const mapping = deriveRepositoryMappingFromResource(resource);
|
|
295
|
+
assert.deepEqual(mapping.outputKeys, ["id", "firstName"]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("deriveRepositoryMappingFromResource throws when view schema properties are missing", () => {
|
|
299
|
+
const resource = {
|
|
300
|
+
operations: {
|
|
301
|
+
view: {
|
|
302
|
+
outputValidator: {
|
|
303
|
+
schema: {
|
|
304
|
+
type: "object"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
create: {
|
|
309
|
+
bodyValidator: {
|
|
310
|
+
schema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
firstName: { type: "string" }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
assert.throws(
|
|
322
|
+
() => deriveRepositoryMappingFromResource(resource),
|
|
323
|
+
/operations\.view\.outputValidator\.schema\.properties/
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("deriveRepositoryMappingFromResource throws when create schema properties are missing", () => {
|
|
328
|
+
const resource = {
|
|
329
|
+
operations: {
|
|
330
|
+
view: {
|
|
331
|
+
outputValidator: {
|
|
332
|
+
schema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
id: { type: "integer" }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
create: {
|
|
341
|
+
bodyValidator: {
|
|
342
|
+
schema: {
|
|
343
|
+
type: "object"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
assert.throws(
|
|
351
|
+
() => deriveRepositoryMappingFromResource(resource),
|
|
352
|
+
/operations\.create\.bodyValidator\.schema\.properties/
|
|
353
|
+
);
|
|
354
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createCrudServiceEvents } from "../src/server/serviceEvents.js";
|
|
4
|
+
|
|
5
|
+
test("createCrudServiceEvents builds CRUD realtime events from resource namespace", () => {
|
|
6
|
+
const events = createCrudServiceEvents({
|
|
7
|
+
resource: "contacts"
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
assert.equal(events.createRecord[0].realtime.event, "contacts.record.changed");
|
|
11
|
+
assert.equal(events.updateRecord[0].realtime.event, "contacts.record.changed");
|
|
12
|
+
assert.equal(events.deleteRecord[0].realtime.event, "contacts.record.changed");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("createCrudServiceEvents normalizes namespace into realtime event format", () => {
|
|
16
|
+
const events = createCrudServiceEvents({
|
|
17
|
+
resource: "customer-orders"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
assert.equal(events.createRecord[0].realtime.event, "customer_orders.record.changed");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("createCrudServiceEvents validates required resource namespace", () => {
|
|
24
|
+
assert.throws(
|
|
25
|
+
() => createCrudServiceEvents({}),
|
|
26
|
+
/resource\.resource/
|
|
27
|
+
);
|
|
28
|
+
});
|