@objectstack/objectql 3.2.1 → 3.2.3
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +37 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/engine.test.ts +46 -46
- package/src/engine.ts +8 -10
- package/src/plugin.integration.test.ts +23 -24
- package/src/plugin.ts +17 -14
- package/src/protocol-data.test.ts +12 -12
- package/src/protocol.ts +17 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/objectql",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Isomorphic ObjectQL Engine for ObjectStack",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/
|
|
17
|
-
"@objectstack/
|
|
18
|
-
"@objectstack/types": "3.2.
|
|
16
|
+
"@objectstack/core": "3.2.3",
|
|
17
|
+
"@objectstack/spec": "3.2.3",
|
|
18
|
+
"@objectstack/types": "3.2.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"typescript": "^5.0.0",
|
package/src/engine.test.ts
CHANGED
|
@@ -239,20 +239,20 @@ describe('ObjectQL Engine', () => {
|
|
|
239
239
|
// Primary find returns tasks with assignee IDs
|
|
240
240
|
vi.mocked(mockDriver.find)
|
|
241
241
|
.mockResolvedValueOnce([
|
|
242
|
-
{
|
|
243
|
-
{
|
|
242
|
+
{ id: 't1', title: 'Task 1', assignee: 'u1' },
|
|
243
|
+
{ id: 't2', title: 'Task 2', assignee: 'u2' },
|
|
244
244
|
])
|
|
245
245
|
// Second call (expand): returns user records
|
|
246
246
|
.mockResolvedValueOnce([
|
|
247
|
-
{
|
|
248
|
-
{
|
|
247
|
+
{ id: 'u1', name: 'Alice' },
|
|
248
|
+
{ id: 'u2', name: 'Bob' },
|
|
249
249
|
]);
|
|
250
250
|
|
|
251
251
|
const result = await engine.find('task', { populate: ['assignee'] });
|
|
252
252
|
|
|
253
253
|
expect(result).toHaveLength(2);
|
|
254
|
-
expect(result[0].assignee).toEqual({
|
|
255
|
-
expect(result[1].assignee).toEqual({
|
|
254
|
+
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
255
|
+
expect(result[1].assignee).toEqual({ id: 'u2', name: 'Bob' });
|
|
256
256
|
|
|
257
257
|
// Verify the expand query used $in
|
|
258
258
|
expect(mockDriver.find).toHaveBeenCalledTimes(2);
|
|
@@ -260,7 +260,7 @@ describe('ObjectQL Engine', () => {
|
|
|
260
260
|
'user',
|
|
261
261
|
expect.objectContaining({
|
|
262
262
|
object: 'user',
|
|
263
|
-
where: {
|
|
263
|
+
where: { id: { $in: ['u1', 'u2'] } },
|
|
264
264
|
}),
|
|
265
265
|
);
|
|
266
266
|
});
|
|
@@ -282,14 +282,14 @@ describe('ObjectQL Engine', () => {
|
|
|
282
282
|
|
|
283
283
|
vi.mocked(mockDriver.find)
|
|
284
284
|
.mockResolvedValueOnce([
|
|
285
|
-
{
|
|
285
|
+
{ id: 'oi1', order: 'o1' },
|
|
286
286
|
])
|
|
287
287
|
.mockResolvedValueOnce([
|
|
288
|
-
{
|
|
288
|
+
{ id: 'o1', total: 100 },
|
|
289
289
|
]);
|
|
290
290
|
|
|
291
291
|
const result = await engine.find('order_item', { populate: ['order'] });
|
|
292
|
-
expect(result[0].order).toEqual({
|
|
292
|
+
expect(result[0].order).toEqual({ id: 'o1', total: 100 });
|
|
293
293
|
});
|
|
294
294
|
|
|
295
295
|
it('should skip expand for fields without reference definition', async () => {
|
|
@@ -301,7 +301,7 @@ describe('ObjectQL Engine', () => {
|
|
|
301
301
|
} as any);
|
|
302
302
|
|
|
303
303
|
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
304
|
-
{
|
|
304
|
+
{ id: 't1', title: 'Task 1' },
|
|
305
305
|
]);
|
|
306
306
|
|
|
307
307
|
const result = await engine.find('task', { populate: ['title'] });
|
|
@@ -313,7 +313,7 @@ describe('ObjectQL Engine', () => {
|
|
|
313
313
|
vi.mocked(SchemaRegistry.getObject).mockReturnValue(undefined);
|
|
314
314
|
|
|
315
315
|
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
316
|
-
{
|
|
316
|
+
{ id: 't1', assignee: 'u1' },
|
|
317
317
|
]);
|
|
318
318
|
|
|
319
319
|
const result = await engine.find('task', { populate: ['assignee'] });
|
|
@@ -338,16 +338,16 @@ describe('ObjectQL Engine', () => {
|
|
|
338
338
|
|
|
339
339
|
vi.mocked(mockDriver.find)
|
|
340
340
|
.mockResolvedValueOnce([
|
|
341
|
-
{
|
|
342
|
-
{
|
|
341
|
+
{ id: 't1', assignee: null },
|
|
342
|
+
{ id: 't2', assignee: 'u1' },
|
|
343
343
|
])
|
|
344
344
|
.mockResolvedValueOnce([
|
|
345
|
-
{
|
|
345
|
+
{ id: 'u1', name: 'Alice' },
|
|
346
346
|
]);
|
|
347
347
|
|
|
348
348
|
const result = await engine.find('task', { populate: ['assignee'] });
|
|
349
349
|
expect(result[0].assignee).toBeNull();
|
|
350
|
-
expect(result[1].assignee).toEqual({
|
|
350
|
+
expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
351
351
|
});
|
|
352
352
|
|
|
353
353
|
it('should de-duplicate foreign key IDs in batch query', async () => {
|
|
@@ -367,13 +367,13 @@ describe('ObjectQL Engine', () => {
|
|
|
367
367
|
|
|
368
368
|
vi.mocked(mockDriver.find)
|
|
369
369
|
.mockResolvedValueOnce([
|
|
370
|
-
{
|
|
371
|
-
{
|
|
372
|
-
{
|
|
370
|
+
{ id: 't1', assignee: 'u1' },
|
|
371
|
+
{ id: 't2', assignee: 'u1' }, // Same user
|
|
372
|
+
{ id: 't3', assignee: 'u2' },
|
|
373
373
|
])
|
|
374
374
|
.mockResolvedValueOnce([
|
|
375
|
-
{
|
|
376
|
-
{
|
|
375
|
+
{ id: 'u1', name: 'Alice' },
|
|
376
|
+
{ id: 'u2', name: 'Bob' },
|
|
377
377
|
]);
|
|
378
378
|
|
|
379
379
|
const result = await engine.find('task', { populate: ['assignee'] });
|
|
@@ -382,11 +382,11 @@ describe('ObjectQL Engine', () => {
|
|
|
382
382
|
expect(mockDriver.find).toHaveBeenLastCalledWith(
|
|
383
383
|
'user',
|
|
384
384
|
expect.objectContaining({
|
|
385
|
-
where: {
|
|
385
|
+
where: { id: { $in: ['u1', 'u2'] } },
|
|
386
386
|
}),
|
|
387
387
|
);
|
|
388
|
-
expect(result[0].assignee).toEqual({
|
|
389
|
-
expect(result[1].assignee).toEqual({
|
|
388
|
+
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
389
|
+
expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
390
390
|
});
|
|
391
391
|
|
|
392
392
|
it('should keep raw ID when referenced record not found', async () => {
|
|
@@ -406,7 +406,7 @@ describe('ObjectQL Engine', () => {
|
|
|
406
406
|
|
|
407
407
|
vi.mocked(mockDriver.find)
|
|
408
408
|
.mockResolvedValueOnce([
|
|
409
|
-
{
|
|
409
|
+
{ id: 't1', assignee: 'u_deleted' },
|
|
410
410
|
])
|
|
411
411
|
.mockResolvedValueOnce([]); // No records found
|
|
412
412
|
|
|
@@ -436,15 +436,15 @@ describe('ObjectQL Engine', () => {
|
|
|
436
436
|
|
|
437
437
|
vi.mocked(mockDriver.find)
|
|
438
438
|
.mockResolvedValueOnce([
|
|
439
|
-
{
|
|
439
|
+
{ id: 't1', assignee: 'u1', project: 'p1' },
|
|
440
440
|
])
|
|
441
|
-
.mockResolvedValueOnce([{
|
|
442
|
-
.mockResolvedValueOnce([{
|
|
441
|
+
.mockResolvedValueOnce([{ id: 'u1', name: 'Alice' }])
|
|
442
|
+
.mockResolvedValueOnce([{ id: 'p1', name: 'Project X' }]);
|
|
443
443
|
|
|
444
444
|
const result = await engine.find('task', { populate: ['assignee', 'project'] });
|
|
445
445
|
|
|
446
|
-
expect(result[0].assignee).toEqual({
|
|
447
|
-
expect(result[0].project).toEqual({
|
|
446
|
+
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
447
|
+
expect(result[0].project).toEqual({ id: 'p1', name: 'Project X' });
|
|
448
448
|
expect(mockDriver.find).toHaveBeenCalledTimes(3);
|
|
449
449
|
});
|
|
450
450
|
|
|
@@ -464,15 +464,15 @@ describe('ObjectQL Engine', () => {
|
|
|
464
464
|
});
|
|
465
465
|
|
|
466
466
|
vi.mocked(mockDriver.findOne as any).mockResolvedValueOnce(
|
|
467
|
-
{
|
|
467
|
+
{ id: 't1', title: 'Task 1', assignee: 'u1' },
|
|
468
468
|
);
|
|
469
469
|
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
470
|
-
{
|
|
470
|
+
{ id: 'u1', name: 'Alice' },
|
|
471
471
|
]);
|
|
472
472
|
|
|
473
473
|
const result = await engine.findOne('task', { populate: ['assignee'] });
|
|
474
474
|
|
|
475
|
-
expect(result.assignee).toEqual({
|
|
475
|
+
expect(result.assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
476
476
|
});
|
|
477
477
|
|
|
478
478
|
it('should handle already-expanded objects (skip re-expansion)', async () => {
|
|
@@ -492,14 +492,14 @@ describe('ObjectQL Engine', () => {
|
|
|
492
492
|
|
|
493
493
|
// Driver returns an already-expanded object
|
|
494
494
|
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
495
|
-
{
|
|
495
|
+
{ id: 't1', assignee: { id: 'u1', name: 'Alice' } },
|
|
496
496
|
]);
|
|
497
497
|
|
|
498
498
|
const result = await engine.find('task', { populate: ['assignee'] });
|
|
499
499
|
|
|
500
500
|
// No expand query should have been made — the value was already an object
|
|
501
501
|
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
502
|
-
expect(result[0].assignee).toEqual({
|
|
502
|
+
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
503
503
|
});
|
|
504
504
|
|
|
505
505
|
it('should gracefully handle expand errors and keep raw IDs', async () => {
|
|
@@ -519,7 +519,7 @@ describe('ObjectQL Engine', () => {
|
|
|
519
519
|
|
|
520
520
|
vi.mocked(mockDriver.find)
|
|
521
521
|
.mockResolvedValueOnce([
|
|
522
|
-
{
|
|
522
|
+
{ id: 't1', assignee: 'u1' },
|
|
523
523
|
])
|
|
524
524
|
.mockRejectedValueOnce(new Error('Driver connection failed'));
|
|
525
525
|
|
|
@@ -544,17 +544,17 @@ describe('ObjectQL Engine', () => {
|
|
|
544
544
|
|
|
545
545
|
vi.mocked(mockDriver.find)
|
|
546
546
|
.mockResolvedValueOnce([
|
|
547
|
-
{
|
|
547
|
+
{ id: 't1', watchers: ['u1', 'u2'] },
|
|
548
548
|
])
|
|
549
549
|
.mockResolvedValueOnce([
|
|
550
|
-
{
|
|
551
|
-
{
|
|
550
|
+
{ id: 'u1', name: 'Alice' },
|
|
551
|
+
{ id: 'u2', name: 'Bob' },
|
|
552
552
|
]);
|
|
553
553
|
|
|
554
554
|
const result = await engine.find('task', { populate: ['watchers'] });
|
|
555
555
|
expect(result[0].watchers).toEqual([
|
|
556
|
-
{
|
|
557
|
-
{
|
|
556
|
+
{ id: 'u1', name: 'Alice' },
|
|
557
|
+
{ id: 'u2', name: 'Bob' },
|
|
558
558
|
]);
|
|
559
559
|
});
|
|
560
560
|
|
|
@@ -570,14 +570,14 @@ describe('ObjectQL Engine', () => {
|
|
|
570
570
|
});
|
|
571
571
|
|
|
572
572
|
vi.mocked(mockDriver.find)
|
|
573
|
-
.mockResolvedValueOnce([{
|
|
574
|
-
.mockResolvedValueOnce([{
|
|
573
|
+
.mockResolvedValueOnce([{ id: 't1', project: 'p1' }]) // find task
|
|
574
|
+
.mockResolvedValueOnce([{ id: 'p1', org: 'o1' }]); // expand project (depth 0)
|
|
575
575
|
// org should NOT be expanded further — flat populate doesn't create nested expand
|
|
576
576
|
|
|
577
577
|
const result = await engine.find('task', { populate: ['project'] });
|
|
578
578
|
|
|
579
579
|
// Project expanded, but org inside project remains as raw ID
|
|
580
|
-
expect(result[0].project).toEqual({
|
|
580
|
+
expect(result[0].project).toEqual({ id: 'p1', org: 'o1' });
|
|
581
581
|
expect(mockDriver.find).toHaveBeenCalledTimes(2); // Only primary + 1 expand query
|
|
582
582
|
});
|
|
583
583
|
|
|
@@ -588,11 +588,11 @@ describe('ObjectQL Engine', () => {
|
|
|
588
588
|
} as any);
|
|
589
589
|
|
|
590
590
|
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
591
|
-
{
|
|
591
|
+
{ id: 't1', title: 'Task 1' },
|
|
592
592
|
]);
|
|
593
593
|
|
|
594
594
|
const result = await engine.find('task', {});
|
|
595
|
-
expect(result).toEqual([{
|
|
595
|
+
expect(result).toEqual([{ id: 't1', title: 'Task 1' }]);
|
|
596
596
|
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
597
597
|
});
|
|
598
598
|
});
|
package/src/engine.ts
CHANGED
|
@@ -681,7 +681,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
681
681
|
try {
|
|
682
682
|
const relatedQuery: QueryAST = {
|
|
683
683
|
object: referenceObject,
|
|
684
|
-
where: {
|
|
684
|
+
where: { id: { $in: uniqueIds } },
|
|
685
685
|
...(nestedAST.fields ? { fields: nestedAST.fields } : {}),
|
|
686
686
|
...(nestedAST.orderBy ? { orderBy: nestedAST.orderBy } : {}),
|
|
687
687
|
};
|
|
@@ -692,7 +692,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
692
692
|
// Build a lookup map: id → record
|
|
693
693
|
const recordMap = new Map<string, any>();
|
|
694
694
|
for (const rec of relatedRecords) {
|
|
695
|
-
const id = rec.
|
|
695
|
+
const id = rec.id;
|
|
696
696
|
if (id != null) recordMap.set(String(id), rec);
|
|
697
697
|
}
|
|
698
698
|
|
|
@@ -707,7 +707,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
707
707
|
// Rebuild map with expanded records
|
|
708
708
|
recordMap.clear();
|
|
709
709
|
for (const rec of expandedRelated) {
|
|
710
|
-
const id = rec.
|
|
710
|
+
const id = rec.id;
|
|
711
711
|
if (id != null) recordMap.set(String(id), rec);
|
|
712
712
|
}
|
|
713
713
|
}
|
|
@@ -921,10 +921,9 @@ export class ObjectQL implements IDataEngine {
|
|
|
921
921
|
const driver = this.getDriver(object);
|
|
922
922
|
|
|
923
923
|
// 1. Extract ID from data or filter if it's a single update by ID
|
|
924
|
-
let id = data.id
|
|
924
|
+
let id = data.id;
|
|
925
925
|
if (!id && options?.filter) {
|
|
926
926
|
if (typeof options.filter === 'string') id = options.filter;
|
|
927
|
-
else if (options.filter._id) id = options.filter._id;
|
|
928
927
|
else if (options.filter.id) id = options.filter.id;
|
|
929
928
|
}
|
|
930
929
|
|
|
@@ -980,7 +979,6 @@ export class ObjectQL implements IDataEngine {
|
|
|
980
979
|
let id: any = undefined;
|
|
981
980
|
if (options?.filter) {
|
|
982
981
|
if (typeof options.filter === 'string') id = options.filter;
|
|
983
|
-
else if (options.filter._id) id = options.filter._id;
|
|
984
982
|
else if (options.filter.id) id = options.filter.id;
|
|
985
983
|
}
|
|
986
984
|
|
|
@@ -1043,7 +1041,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
1043
1041
|
return driver.count(object, ast);
|
|
1044
1042
|
}
|
|
1045
1043
|
// Fallback to find().length
|
|
1046
|
-
const res = await this.find(object, { filter: query?.filter, select: ['
|
|
1044
|
+
const res = await this.find(object, { filter: query?.filter, select: ['id'] });
|
|
1047
1045
|
return res.length;
|
|
1048
1046
|
});
|
|
1049
1047
|
|
|
@@ -1335,8 +1333,8 @@ export class ObjectRepository {
|
|
|
1335
1333
|
|
|
1336
1334
|
/** Update a single record by ID */
|
|
1337
1335
|
async updateById(id: string | number, data: any): Promise<any> {
|
|
1338
|
-
return this.engine.update(this.objectName, { ...data,
|
|
1339
|
-
filter: {
|
|
1336
|
+
return this.engine.update(this.objectName, { ...data, id: id }, {
|
|
1337
|
+
filter: { id: id },
|
|
1340
1338
|
context: this.context,
|
|
1341
1339
|
});
|
|
1342
1340
|
}
|
|
@@ -1351,7 +1349,7 @@ export class ObjectRepository {
|
|
|
1351
1349
|
/** Delete a single record by ID */
|
|
1352
1350
|
async deleteById(id: string | number): Promise<any> {
|
|
1353
1351
|
return this.engine.delete(this.objectName, {
|
|
1354
|
-
filter: {
|
|
1352
|
+
filter: { id: id },
|
|
1355
1353
|
context: this.context,
|
|
1356
1354
|
});
|
|
1357
1355
|
}
|
|
@@ -16,7 +16,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
16
16
|
it('should register ObjectQL as metadata service provider', async () => {
|
|
17
17
|
// Arrange
|
|
18
18
|
const plugin = new ObjectQLPlugin();
|
|
19
|
-
kernel.use(plugin);
|
|
19
|
+
await kernel.use(plugin);
|
|
20
20
|
|
|
21
21
|
// Act
|
|
22
22
|
await kernel.bootstrap();
|
|
@@ -24,16 +24,19 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
24
24
|
// Assert
|
|
25
25
|
const metadataService = kernel.getService('metadata');
|
|
26
26
|
expect(metadataService).toBeDefined();
|
|
27
|
-
|
|
28
|
-
//
|
|
27
|
+
|
|
28
|
+
// ObjectQL registers a MetadataFacade as the metadata service;
|
|
29
|
+
// it is separate from (but backed by the same registry as) the objectql service.
|
|
29
30
|
const objectql = kernel.getService('objectql');
|
|
30
|
-
expect(
|
|
31
|
+
expect(objectql).toBeDefined();
|
|
32
|
+
// metadata and objectql are distinct service instances
|
|
33
|
+
expect(metadataService).not.toBe(objectql);
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
it('should serve in-memory metadata definitions', async () => {
|
|
34
37
|
// Arrange
|
|
35
38
|
const plugin = new ObjectQLPlugin();
|
|
36
|
-
kernel.use(plugin);
|
|
39
|
+
await kernel.use(plugin);
|
|
37
40
|
await kernel.bootstrap();
|
|
38
41
|
|
|
39
42
|
const objectql = kernel.getService('objectql') as any;
|
|
@@ -49,17 +52,13 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
49
52
|
}
|
|
50
53
|
};
|
|
51
54
|
|
|
52
|
-
// Act - Register object programmatically
|
|
53
|
-
objectql.registry.registerObject(
|
|
54
|
-
packageId: 'test',
|
|
55
|
-
namespace: 'test',
|
|
56
|
-
ownership: 'own',
|
|
57
|
-
object: testObject
|
|
58
|
-
});
|
|
55
|
+
// Act - Register object programmatically via the SchemaRegistry API
|
|
56
|
+
objectql.registry.registerObject(testObject, 'test', 'test');
|
|
59
57
|
|
|
60
|
-
// Assert - Should be retrievable via registry
|
|
61
|
-
const objects = objectql.registry.
|
|
62
|
-
|
|
58
|
+
// Assert - Should be retrievable via registry (getAllObjects returns ServiceObject[])
|
|
59
|
+
const objects = objectql.registry.getAllObjects();
|
|
60
|
+
const fqns = objects.map((o: any) => o.name);
|
|
61
|
+
expect(fqns).toContain('test__test_object');
|
|
63
62
|
});
|
|
64
63
|
});
|
|
65
64
|
|
|
@@ -67,7 +66,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
67
66
|
it('should register objectql, data, and protocol services', async () => {
|
|
68
67
|
// Arrange
|
|
69
68
|
const plugin = new ObjectQLPlugin();
|
|
70
|
-
kernel.use(plugin);
|
|
69
|
+
await kernel.use(plugin);
|
|
71
70
|
|
|
72
71
|
// Act
|
|
73
72
|
await kernel.bootstrap();
|
|
@@ -88,7 +87,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
88
87
|
list: async () => []
|
|
89
88
|
};
|
|
90
89
|
|
|
91
|
-
kernel.use({
|
|
90
|
+
await kernel.use({
|
|
92
91
|
name: 'mock-metadata',
|
|
93
92
|
type: 'test',
|
|
94
93
|
version: '1.0.0',
|
|
@@ -98,7 +97,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
const plugin = new ObjectQLPlugin();
|
|
101
|
-
kernel.use(plugin);
|
|
100
|
+
await kernel.use(plugin);
|
|
102
101
|
|
|
103
102
|
// Act
|
|
104
103
|
await kernel.bootstrap();
|
|
@@ -125,7 +124,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
125
124
|
delete: async () => ({ count: 1 })
|
|
126
125
|
};
|
|
127
126
|
|
|
128
|
-
kernel.use({
|
|
127
|
+
await kernel.use({
|
|
129
128
|
name: 'mock-driver-plugin',
|
|
130
129
|
type: 'driver',
|
|
131
130
|
version: '1.0.0',
|
|
@@ -135,7 +134,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
135
134
|
});
|
|
136
135
|
|
|
137
136
|
const plugin = new ObjectQLPlugin();
|
|
138
|
-
kernel.use(plugin);
|
|
137
|
+
await kernel.use(plugin);
|
|
139
138
|
|
|
140
139
|
// Act
|
|
141
140
|
await kernel.bootstrap();
|
|
@@ -156,7 +155,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
156
155
|
}
|
|
157
156
|
};
|
|
158
157
|
|
|
159
|
-
kernel.use({
|
|
158
|
+
await kernel.use({
|
|
160
159
|
name: 'mock-app-plugin',
|
|
161
160
|
type: 'app',
|
|
162
161
|
version: '1.0.0',
|
|
@@ -166,7 +165,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
166
165
|
});
|
|
167
166
|
|
|
168
167
|
const plugin = new ObjectQLPlugin();
|
|
169
|
-
kernel.use(plugin);
|
|
168
|
+
await kernel.use(plugin);
|
|
170
169
|
|
|
171
170
|
// Act
|
|
172
171
|
await kernel.bootstrap();
|
|
@@ -212,7 +211,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
212
211
|
};
|
|
213
212
|
|
|
214
213
|
// Register mock metadata service BEFORE ObjectQL
|
|
215
|
-
kernel.use({
|
|
214
|
+
await kernel.use({
|
|
216
215
|
name: 'mock-metadata',
|
|
217
216
|
type: 'metadata',
|
|
218
217
|
version: '1.0.0',
|
|
@@ -222,7 +221,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
222
221
|
});
|
|
223
222
|
|
|
224
223
|
const plugin = new ObjectQLPlugin();
|
|
225
|
-
kernel.use(plugin);
|
|
224
|
+
await kernel.use(plugin);
|
|
226
225
|
|
|
227
226
|
// Act
|
|
228
227
|
await kernel.bootstrap();
|
package/src/plugin.ts
CHANGED
|
@@ -116,6 +116,9 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Initialize drivers (calls driver.connect() which sets up persistence)
|
|
120
|
+
await this.ql?.init();
|
|
121
|
+
|
|
119
122
|
// Register built-in audit hooks
|
|
120
123
|
this.registerAuditHooks(ctx);
|
|
121
124
|
|
|
@@ -129,35 +132,35 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
|
-
* Register built-in audit hooks for auto-stamping
|
|
135
|
+
* Register built-in audit hooks for auto-stamping created_by/updated_by
|
|
133
136
|
* and fetching previousData for update/delete operations.
|
|
134
137
|
*/
|
|
135
138
|
private registerAuditHooks(ctx: PluginContext) {
|
|
136
139
|
if (!this.ql) return;
|
|
137
140
|
|
|
138
|
-
// Auto-stamp
|
|
141
|
+
// Auto-stamp created_by/updated_by on insert
|
|
139
142
|
this.ql.registerHook('beforeInsert', async (hookCtx) => {
|
|
140
143
|
if (hookCtx.session?.userId && hookCtx.input?.data) {
|
|
141
144
|
const data = hookCtx.input.data as Record<string, any>;
|
|
142
145
|
if (typeof data === 'object' && data !== null) {
|
|
143
146
|
data.created_by = data.created_by ?? hookCtx.session.userId;
|
|
144
|
-
data.
|
|
147
|
+
data.updated_by = hookCtx.session.userId;
|
|
145
148
|
data.created_at = data.created_at ?? new Date().toISOString();
|
|
146
|
-
data.
|
|
149
|
+
data.updated_at = new Date().toISOString();
|
|
147
150
|
if (hookCtx.session.tenantId) {
|
|
148
|
-
data.
|
|
151
|
+
data.tenant_id = data.tenant_id ?? hookCtx.session.tenantId;
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
}, { object: '*', priority: 10 });
|
|
153
156
|
|
|
154
|
-
// Auto-stamp
|
|
157
|
+
// Auto-stamp updated_by on update
|
|
155
158
|
this.ql.registerHook('beforeUpdate', async (hookCtx) => {
|
|
156
159
|
if (hookCtx.session?.userId && hookCtx.input?.data) {
|
|
157
160
|
const data = hookCtx.input.data as Record<string, any>;
|
|
158
161
|
if (typeof data === 'object' && data !== null) {
|
|
159
|
-
data.
|
|
160
|
-
data.
|
|
162
|
+
data.updated_by = hookCtx.session.userId;
|
|
163
|
+
data.updated_at = new Date().toISOString();
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
}, { object: '*', priority: 10 });
|
|
@@ -167,7 +170,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
167
170
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
168
171
|
try {
|
|
169
172
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
170
|
-
filter: {
|
|
173
|
+
filter: { id: hookCtx.input.id }
|
|
171
174
|
});
|
|
172
175
|
if (existing) {
|
|
173
176
|
hookCtx.previous = existing;
|
|
@@ -183,7 +186,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
183
186
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
184
187
|
try {
|
|
185
188
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
186
|
-
filter: {
|
|
189
|
+
filter: { id: hookCtx.input.id }
|
|
187
190
|
});
|
|
188
191
|
if (existing) {
|
|
189
192
|
hookCtx.previous = existing;
|
|
@@ -194,11 +197,11 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
194
197
|
}
|
|
195
198
|
}, { object: '*', priority: 5 });
|
|
196
199
|
|
|
197
|
-
ctx.logger.debug('Audit hooks registered (
|
|
200
|
+
ctx.logger.debug('Audit hooks registered (created_by/updated_by, previousData)');
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
/**
|
|
201
|
-
* Register tenant isolation middleware that auto-injects
|
|
204
|
+
* Register tenant isolation middleware that auto-injects tenant_id filter
|
|
202
205
|
* for multi-tenant operations.
|
|
203
206
|
*/
|
|
204
207
|
private registerTenantMiddleware(ctx: PluginContext) {
|
|
@@ -210,10 +213,10 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
210
213
|
return next();
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
// Read operations: inject
|
|
216
|
+
// Read operations: inject tenant_id filter into AST
|
|
214
217
|
if (['find', 'findOne', 'count', 'aggregate'].includes(opCtx.operation)) {
|
|
215
218
|
if (opCtx.ast) {
|
|
216
|
-
const tenantFilter = {
|
|
219
|
+
const tenantFilter = { tenant_id: opCtx.context.tenantId };
|
|
217
220
|
if (opCtx.ast.where) {
|
|
218
221
|
opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] };
|
|
219
222
|
} else {
|