@objectstack/objectql 3.2.0 → 3.2.2
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 +9 -9
- package/CHANGELOG.md +18 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +36 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -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.ts +14 -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.2",
|
|
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/core": "3.2.
|
|
17
|
-
"@objectstack/spec": "3.2.
|
|
18
|
-
"@objectstack/types": "3.2.
|
|
16
|
+
"@objectstack/core": "3.2.2",
|
|
17
|
+
"@objectstack/spec": "3.2.2",
|
|
18
|
+
"@objectstack/types": "3.2.2"
|
|
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
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -129,35 +129,35 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Register built-in audit hooks for auto-stamping
|
|
132
|
+
* Register built-in audit hooks for auto-stamping created_by/updated_by
|
|
133
133
|
* and fetching previousData for update/delete operations.
|
|
134
134
|
*/
|
|
135
135
|
private registerAuditHooks(ctx: PluginContext) {
|
|
136
136
|
if (!this.ql) return;
|
|
137
137
|
|
|
138
|
-
// Auto-stamp
|
|
138
|
+
// Auto-stamp created_by/updated_by on insert
|
|
139
139
|
this.ql.registerHook('beforeInsert', async (hookCtx) => {
|
|
140
140
|
if (hookCtx.session?.userId && hookCtx.input?.data) {
|
|
141
141
|
const data = hookCtx.input.data as Record<string, any>;
|
|
142
142
|
if (typeof data === 'object' && data !== null) {
|
|
143
143
|
data.created_by = data.created_by ?? hookCtx.session.userId;
|
|
144
|
-
data.
|
|
144
|
+
data.updated_by = hookCtx.session.userId;
|
|
145
145
|
data.created_at = data.created_at ?? new Date().toISOString();
|
|
146
|
-
data.
|
|
146
|
+
data.updated_at = new Date().toISOString();
|
|
147
147
|
if (hookCtx.session.tenantId) {
|
|
148
|
-
data.
|
|
148
|
+
data.tenant_id = data.tenant_id ?? hookCtx.session.tenantId;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
}, { object: '*', priority: 10 });
|
|
153
153
|
|
|
154
|
-
// Auto-stamp
|
|
154
|
+
// Auto-stamp updated_by on update
|
|
155
155
|
this.ql.registerHook('beforeUpdate', async (hookCtx) => {
|
|
156
156
|
if (hookCtx.session?.userId && hookCtx.input?.data) {
|
|
157
157
|
const data = hookCtx.input.data as Record<string, any>;
|
|
158
158
|
if (typeof data === 'object' && data !== null) {
|
|
159
|
-
data.
|
|
160
|
-
data.
|
|
159
|
+
data.updated_by = hookCtx.session.userId;
|
|
160
|
+
data.updated_at = new Date().toISOString();
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}, { object: '*', priority: 10 });
|
|
@@ -167,7 +167,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
167
167
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
168
168
|
try {
|
|
169
169
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
170
|
-
filter: {
|
|
170
|
+
filter: { id: hookCtx.input.id }
|
|
171
171
|
});
|
|
172
172
|
if (existing) {
|
|
173
173
|
hookCtx.previous = existing;
|
|
@@ -183,7 +183,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
183
183
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
184
184
|
try {
|
|
185
185
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
186
|
-
filter: {
|
|
186
|
+
filter: { id: hookCtx.input.id }
|
|
187
187
|
});
|
|
188
188
|
if (existing) {
|
|
189
189
|
hookCtx.previous = existing;
|
|
@@ -194,11 +194,11 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
194
194
|
}
|
|
195
195
|
}, { object: '*', priority: 5 });
|
|
196
196
|
|
|
197
|
-
ctx.logger.debug('Audit hooks registered (
|
|
197
|
+
ctx.logger.debug('Audit hooks registered (created_by/updated_by, previousData)');
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
|
-
* Register tenant isolation middleware that auto-injects
|
|
201
|
+
* Register tenant isolation middleware that auto-injects tenant_id filter
|
|
202
202
|
* for multi-tenant operations.
|
|
203
203
|
*/
|
|
204
204
|
private registerTenantMiddleware(ctx: PluginContext) {
|
|
@@ -210,10 +210,10 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
210
210
|
return next();
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
// Read operations: inject
|
|
213
|
+
// Read operations: inject tenant_id filter into AST
|
|
214
214
|
if (['find', 'findOne', 'count', 'aggregate'].includes(opCtx.operation)) {
|
|
215
215
|
if (opCtx.ast) {
|
|
216
|
-
const tenantFilter = {
|
|
216
|
+
const tenantFilter = { tenant_id: opCtx.context.tenantId };
|
|
217
217
|
if (opCtx.ast.where) {
|
|
218
218
|
opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] };
|
|
219
219
|
} else {
|
|
@@ -129,14 +129,14 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
it('should return records and standard response shape', async () => {
|
|
132
|
-
mockEngine.find.mockResolvedValue([{
|
|
132
|
+
mockEngine.find.mockResolvedValue([{ id: 't1', name: 'Task 1' }]);
|
|
133
133
|
|
|
134
134
|
const result = await protocol.findData({ object: 'task', query: {} });
|
|
135
135
|
|
|
136
136
|
expect(result).toEqual(
|
|
137
137
|
expect.objectContaining({
|
|
138
138
|
object: 'task',
|
|
139
|
-
records: [{
|
|
139
|
+
records: [{ id: 't1', name: 'Task 1' }],
|
|
140
140
|
total: 1,
|
|
141
141
|
}),
|
|
142
142
|
);
|
|
@@ -149,21 +149,21 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
149
149
|
|
|
150
150
|
describe('getData', () => {
|
|
151
151
|
it('should convert expand string to populate array', async () => {
|
|
152
|
-
mockEngine.findOne.mockResolvedValue({
|
|
152
|
+
mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' });
|
|
153
153
|
|
|
154
154
|
await protocol.getData({ object: 'order_item', id: 'oi_1', expand: 'order,product' });
|
|
155
155
|
|
|
156
156
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
157
157
|
'order_item',
|
|
158
158
|
expect.objectContaining({
|
|
159
|
-
filter: {
|
|
159
|
+
filter: { id: 'oi_1' },
|
|
160
160
|
populate: ['order', 'product'],
|
|
161
161
|
}),
|
|
162
162
|
);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('should convert expand array to populate array', async () => {
|
|
166
|
-
mockEngine.findOne.mockResolvedValue({
|
|
166
|
+
mockEngine.findOne.mockResolvedValue({ id: 't1' });
|
|
167
167
|
|
|
168
168
|
await protocol.getData({ object: 'task', id: 't1', expand: ['assignee', 'project'] });
|
|
169
169
|
|
|
@@ -176,7 +176,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('should convert select string to array', async () => {
|
|
179
|
-
mockEngine.findOne.mockResolvedValue({
|
|
179
|
+
mockEngine.findOne.mockResolvedValue({ id: 't1', name: 'Test' });
|
|
180
180
|
|
|
181
181
|
await protocol.getData({ object: 'task', id: 't1', select: 'name,status' });
|
|
182
182
|
|
|
@@ -189,7 +189,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
it('should pass both expand and select together', async () => {
|
|
192
|
-
mockEngine.findOne.mockResolvedValue({
|
|
192
|
+
mockEngine.findOne.mockResolvedValue({ id: 'oi_1' });
|
|
193
193
|
|
|
194
194
|
await protocol.getData({
|
|
195
195
|
object: 'order_item',
|
|
@@ -201,7 +201,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
201
201
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
202
202
|
'order_item',
|
|
203
203
|
expect.objectContaining({
|
|
204
|
-
filter: {
|
|
204
|
+
filter: { id: 'oi_1' },
|
|
205
205
|
populate: ['order'],
|
|
206
206
|
select: ['name', 'total'],
|
|
207
207
|
}),
|
|
@@ -209,25 +209,25 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
it('should work without expand or select', async () => {
|
|
212
|
-
mockEngine.findOne.mockResolvedValue({
|
|
212
|
+
mockEngine.findOne.mockResolvedValue({ id: 't1' });
|
|
213
213
|
|
|
214
214
|
await protocol.getData({ object: 'task', id: 't1' });
|
|
215
215
|
|
|
216
216
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
217
217
|
'task',
|
|
218
|
-
{ filter: {
|
|
218
|
+
{ filter: { id: 't1' } },
|
|
219
219
|
);
|
|
220
220
|
});
|
|
221
221
|
|
|
222
222
|
it('should return standard GetDataResponse shape', async () => {
|
|
223
|
-
mockEngine.findOne.mockResolvedValue({
|
|
223
|
+
mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' });
|
|
224
224
|
|
|
225
225
|
const result = await protocol.getData({ object: 'order_item', id: 'oi_1' });
|
|
226
226
|
|
|
227
227
|
expect(result).toEqual({
|
|
228
228
|
object: 'order_item',
|
|
229
229
|
id: 'oi_1',
|
|
230
|
-
record: {
|
|
230
|
+
record: { id: 'oi_1', name: 'Item 1' },
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
package/src/protocol.ts
CHANGED
|
@@ -246,7 +246,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
246
246
|
// Form View Generation
|
|
247
247
|
// Simple single-section layout for now
|
|
248
248
|
const formFields = fieldKeys
|
|
249
|
-
.filter(k => k !== 'id' && k !== 'created_at' && k !== '
|
|
249
|
+
.filter(k => k !== 'id' && k !== 'created_at' && k !== 'updated_at' && !fields[k].hidden)
|
|
250
250
|
.map(f => ({
|
|
251
251
|
field: f,
|
|
252
252
|
label: fields[f]?.label,
|
|
@@ -361,7 +361,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
361
361
|
|
|
362
362
|
async getData(request: { object: string, id: string, expand?: string | string[], select?: string | string[] }) {
|
|
363
363
|
const queryOptions: any = {
|
|
364
|
-
filter: {
|
|
364
|
+
filter: { id: request.id }
|
|
365
365
|
};
|
|
366
366
|
|
|
367
367
|
// Support select for single-record retrieval
|
|
@@ -393,14 +393,14 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
393
393
|
const result = await this.engine.insert(request.object, request.data);
|
|
394
394
|
return {
|
|
395
395
|
object: request.object,
|
|
396
|
-
id: result.
|
|
396
|
+
id: result.id,
|
|
397
397
|
record: result
|
|
398
398
|
};
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
async updateData(request: { object: string, id: string, data: any }) {
|
|
402
402
|
// Adapt: update(obj, id, data) -> update(obj, data, options)
|
|
403
|
-
const result = await this.engine.update(request.object, request.data, { filter: {
|
|
403
|
+
const result = await this.engine.update(request.object, request.data, { filter: { id: request.id } });
|
|
404
404
|
return {
|
|
405
405
|
object: request.object,
|
|
406
406
|
id: request.id,
|
|
@@ -410,7 +410,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
410
410
|
|
|
411
411
|
async deleteData(request: { object: string, id: string }) {
|
|
412
412
|
// Adapt: delete(obj, id) -> delete(obj, options)
|
|
413
|
-
await this.engine.delete(request.object, { filter: {
|
|
413
|
+
await this.engine.delete(request.object, { filter: { id: request.id } });
|
|
414
414
|
return {
|
|
415
415
|
object: request.object,
|
|
416
416
|
id: request.id,
|
|
@@ -478,13 +478,13 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
478
478
|
switch (operation) {
|
|
479
479
|
case 'create': {
|
|
480
480
|
const created = await this.engine.insert(object, record.data || record);
|
|
481
|
-
results.push({ id: created.
|
|
481
|
+
results.push({ id: created.id, success: true, record: created });
|
|
482
482
|
succeeded++;
|
|
483
483
|
break;
|
|
484
484
|
}
|
|
485
485
|
case 'update': {
|
|
486
486
|
if (!record.id) throw new Error('Record id is required for update');
|
|
487
|
-
const updated = await this.engine.update(object, record.data || {}, { filter: {
|
|
487
|
+
const updated = await this.engine.update(object, record.data || {}, { filter: { id: record.id } });
|
|
488
488
|
results.push({ id: record.id, success: true, record: updated });
|
|
489
489
|
succeeded++;
|
|
490
490
|
break;
|
|
@@ -493,28 +493,28 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
493
493
|
// Try update first, then create if not found
|
|
494
494
|
if (record.id) {
|
|
495
495
|
try {
|
|
496
|
-
const existing = await this.engine.findOne(object, { filter: {
|
|
496
|
+
const existing = await this.engine.findOne(object, { filter: { id: record.id } });
|
|
497
497
|
if (existing) {
|
|
498
|
-
const updated = await this.engine.update(object, record.data || {}, { filter: {
|
|
498
|
+
const updated = await this.engine.update(object, record.data || {}, { filter: { id: record.id } });
|
|
499
499
|
results.push({ id: record.id, success: true, record: updated });
|
|
500
500
|
} else {
|
|
501
|
-
const created = await this.engine.insert(object, {
|
|
502
|
-
results.push({ id: created.
|
|
501
|
+
const created = await this.engine.insert(object, { id: record.id, ...(record.data || {}) });
|
|
502
|
+
results.push({ id: created.id, success: true, record: created });
|
|
503
503
|
}
|
|
504
504
|
} catch {
|
|
505
|
-
const created = await this.engine.insert(object, {
|
|
506
|
-
results.push({ id: created.
|
|
505
|
+
const created = await this.engine.insert(object, { id: record.id, ...(record.data || {}) });
|
|
506
|
+
results.push({ id: created.id, success: true, record: created });
|
|
507
507
|
}
|
|
508
508
|
} else {
|
|
509
509
|
const created = await this.engine.insert(object, record.data || record);
|
|
510
|
-
results.push({ id: created.
|
|
510
|
+
results.push({ id: created.id, success: true, record: created });
|
|
511
511
|
}
|
|
512
512
|
succeeded++;
|
|
513
513
|
break;
|
|
514
514
|
}
|
|
515
515
|
case 'delete': {
|
|
516
516
|
if (!record.id) throw new Error('Record id is required for delete');
|
|
517
|
-
await this.engine.delete(object, { filter: {
|
|
517
|
+
await this.engine.delete(object, { filter: { id: record.id } });
|
|
518
518
|
results.push({ id: record.id, success: true });
|
|
519
519
|
succeeded++;
|
|
520
520
|
break;
|
|
@@ -563,7 +563,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
563
563
|
|
|
564
564
|
for (const record of records) {
|
|
565
565
|
try {
|
|
566
|
-
const updated = await this.engine.update(object, record.data, { filter: {
|
|
566
|
+
const updated = await this.engine.update(object, record.data, { filter: { id: record.id } });
|
|
567
567
|
results.push({ id: record.id, success: true, record: updated });
|
|
568
568
|
succeeded++;
|
|
569
569
|
} catch (err: any) {
|
|
@@ -765,7 +765,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
765
765
|
async deleteManyData(request: DeleteManyDataRequest): Promise<any> {
|
|
766
766
|
// This expects deleting by IDs.
|
|
767
767
|
return this.engine.delete(request.object, {
|
|
768
|
-
filter: {
|
|
768
|
+
filter: { id: { $in: request.ids } },
|
|
769
769
|
...request.options
|
|
770
770
|
});
|
|
771
771
|
}
|