@objectstack/objectql 3.3.0 → 4.0.0

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.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/objectql",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Isomorphic ObjectQL Engine for ObjectStack",
6
6
  "main": "dist/index.js",
@@ -13,13 +13,13 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "3.3.0",
17
- "@objectstack/spec": "3.3.0",
18
- "@objectstack/types": "3.3.0"
16
+ "@objectstack/core": "4.0.0",
17
+ "@objectstack/spec": "4.0.0",
18
+ "@objectstack/types": "4.0.0"
19
19
  },
20
20
  "devDependencies": {
21
- "typescript": "^5.0.0",
22
- "vitest": "^4.1.0"
21
+ "typescript": "^6.0.2",
22
+ "vitest": "^4.1.2"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsup --config ../../tsup.config.ts",
@@ -248,7 +248,7 @@ describe('ObjectQL Engine', () => {
248
248
  { id: 'u2', name: 'Bob' },
249
249
  ]);
250
250
 
251
- const result = await engine.find('task', { populate: ['assignee'] });
251
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
252
252
 
253
253
  expect(result).toHaveLength(2);
254
254
  expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
@@ -288,7 +288,7 @@ describe('ObjectQL Engine', () => {
288
288
  { id: 'o1', total: 100 },
289
289
  ]);
290
290
 
291
- const result = await engine.find('order_item', { populate: ['order'] });
291
+ const result = await engine.find('order_item', { expand: { order: { object: 'order' } } });
292
292
  expect(result[0].order).toEqual({ id: 'o1', total: 100 });
293
293
  });
294
294
 
@@ -304,7 +304,7 @@ describe('ObjectQL Engine', () => {
304
304
  { id: 't1', title: 'Task 1' },
305
305
  ]);
306
306
 
307
- const result = await engine.find('task', { populate: ['title'] });
307
+ const result = await engine.find('task', { expand: { title: { object: 'title' } } });
308
308
  expect(result[0].title).toBe('Task 1'); // Unchanged
309
309
  expect(mockDriver.find).toHaveBeenCalledTimes(1); // No expand query
310
310
  });
@@ -316,7 +316,7 @@ describe('ObjectQL Engine', () => {
316
316
  { id: 't1', assignee: 'u1' },
317
317
  ]);
318
318
 
319
- const result = await engine.find('task', { populate: ['assignee'] });
319
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
320
320
  expect(result[0].assignee).toBe('u1'); // Unchanged — raw ID
321
321
  expect(mockDriver.find).toHaveBeenCalledTimes(1);
322
322
  });
@@ -345,7 +345,7 @@ describe('ObjectQL Engine', () => {
345
345
  { id: 'u1', name: 'Alice' },
346
346
  ]);
347
347
 
348
- const result = await engine.find('task', { populate: ['assignee'] });
348
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
349
349
  expect(result[0].assignee).toBeNull();
350
350
  expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' });
351
351
  });
@@ -376,7 +376,7 @@ describe('ObjectQL Engine', () => {
376
376
  { id: 'u2', name: 'Bob' },
377
377
  ]);
378
378
 
379
- const result = await engine.find('task', { populate: ['assignee'] });
379
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
380
380
 
381
381
  // Verify only 2 unique IDs queried
382
382
  expect(mockDriver.find).toHaveBeenLastCalledWith(
@@ -410,7 +410,7 @@ describe('ObjectQL Engine', () => {
410
410
  ])
411
411
  .mockResolvedValueOnce([]); // No records found
412
412
 
413
- const result = await engine.find('task', { populate: ['assignee'] });
413
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
414
414
  expect(result[0].assignee).toBe('u_deleted'); // Fallback to raw ID
415
415
  });
416
416
 
@@ -441,7 +441,7 @@ describe('ObjectQL Engine', () => {
441
441
  .mockResolvedValueOnce([{ id: 'u1', name: 'Alice' }])
442
442
  .mockResolvedValueOnce([{ id: 'p1', name: 'Project X' }]);
443
443
 
444
- const result = await engine.find('task', { populate: ['assignee', 'project'] });
444
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' }, project: { object: 'project' } } });
445
445
 
446
446
  expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
447
447
  expect(result[0].project).toEqual({ id: 'p1', name: 'Project X' });
@@ -470,7 +470,7 @@ describe('ObjectQL Engine', () => {
470
470
  { id: 'u1', name: 'Alice' },
471
471
  ]);
472
472
 
473
- const result = await engine.findOne('task', { populate: ['assignee'] });
473
+ const result = await engine.findOne('task', { expand: { assignee: { object: 'assignee' } } });
474
474
 
475
475
  expect(result.assignee).toEqual({ id: 'u1', name: 'Alice' });
476
476
  });
@@ -495,7 +495,7 @@ describe('ObjectQL Engine', () => {
495
495
  { id: 't1', assignee: { id: 'u1', name: 'Alice' } },
496
496
  ]);
497
497
 
498
- const result = await engine.find('task', { populate: ['assignee'] });
498
+ const result = await engine.find('task', { expand: { assignee: { object: '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);
@@ -523,7 +523,7 @@ describe('ObjectQL Engine', () => {
523
523
  ])
524
524
  .mockRejectedValueOnce(new Error('Driver connection failed'));
525
525
 
526
- const result = await engine.find('task', { populate: ['assignee'] });
526
+ const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
527
527
  expect(result[0].assignee).toBe('u1'); // Kept raw ID
528
528
  });
529
529
 
@@ -551,7 +551,7 @@ describe('ObjectQL Engine', () => {
551
551
  { id: 'u2', name: 'Bob' },
552
552
  ]);
553
553
 
554
- const result = await engine.find('task', { populate: ['watchers'] });
554
+ const result = await engine.find('task', { expand: { watchers: { object: 'watchers' } } });
555
555
  expect(result[0].watchers).toEqual([
556
556
  { id: 'u1', name: 'Alice' },
557
557
  { id: 'u2', name: 'Bob' },
@@ -574,7 +574,7 @@ describe('ObjectQL Engine', () => {
574
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
- const result = await engine.find('task', { populate: ['project'] });
577
+ const result = await engine.find('task', { expand: { project: { object: 'project' } } });
578
578
 
579
579
  // Project expanded, but org inside project remains as raw ID
580
580
  expect(result[0].project).toEqual({ id: 'p1', org: 'o1' });
package/src/engine.ts CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  import { QueryAST, HookContext, ServiceObject } from '@objectstack/spec/data';
4
4
  import {
5
- DataEngineQueryOptions,
5
+ EngineQueryOptions,
6
6
  DataEngineInsertOptions,
7
- DataEngineUpdateOptions,
8
- DataEngineDeleteOptions,
9
- DataEngineAggregateOptions,
10
- DataEngineCountOptions
7
+ EngineUpdateOptions,
8
+ EngineDeleteOptions,
9
+ EngineAggregateOptions,
10
+ EngineCountOptions
11
11
  } from '@objectstack/spec/data';
12
12
  import { ExecutionContext, ExecutionContextSchema } from '@objectstack/spec/kernel';
13
13
  import { DriverInterface, IDataEngine, Logger, createLogger } from '@objectstack/core';
@@ -741,59 +741,22 @@ export class ObjectQL implements IDataEngine {
741
741
  return records;
742
742
  }
743
743
 
744
- // ============================================
745
- // Helper: Query Conversion
746
- // ============================================
747
-
748
- private toQueryAST(object: string, options?: DataEngineQueryOptions): QueryAST {
749
- const ast: QueryAST = { object };
750
- if (!options) return ast;
751
-
752
- if (options.filter) {
753
- ast.where = options.filter;
754
- }
755
- if (options.select) {
756
- ast.fields = options.select;
757
- }
758
- if (options.sort) {
759
- // Support DataEngineSortSchema variant
760
- if (Array.isArray(options.sort)) {
761
- // [{ field: 'a', order: 'asc' }]
762
- ast.orderBy = options.sort;
763
- } else {
764
- // Record<string, 'asc' | 'desc' | 1 | -1>
765
- ast.orderBy = Object.entries(options.sort).map(([field, order]) => ({
766
- field,
767
- order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
768
- }));
769
- }
770
- }
771
-
772
- if (options.top !== undefined) ast.limit = options.top;
773
- else if (options.limit !== undefined) ast.limit = options.limit;
774
-
775
- if (options.skip !== undefined) ast.offset = options.skip;
776
-
777
- // Map populate (relationship field names) to QueryAST expand entries
778
- if (options.populate && options.populate.length > 0) {
779
- ast.expand = {};
780
- for (const rel of options.populate) {
781
- ast.expand[rel] = { object: rel };
782
- }
783
- }
784
-
785
- return ast;
786
- }
787
-
788
744
  // ============================================
789
745
  // Data Access Methods (IDataEngine Interface)
790
746
  // ============================================
791
747
 
792
- async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
748
+ async find(object: string, query?: EngineQueryOptions): Promise<any[]> {
793
749
  object = this.resolveObjectName(object);
794
750
  this.logger.debug('Find operation starting', { object, query });
795
751
  const driver = this.getDriver(object);
796
- const ast = this.toQueryAST(object, query);
752
+ const ast: QueryAST = { object, ...query };
753
+ // Remove context from the AST — it's not a driver concern
754
+ delete (ast as any).context;
755
+ // Normalize OData `top` alias → standard `limit`
756
+ if ((ast as any).top != null && ast.limit == null) {
757
+ ast.limit = (ast as any).top;
758
+ }
759
+ delete (ast as any).top;
797
760
 
798
761
  const opCtx: OperationContext = {
799
762
  object,
@@ -836,12 +799,14 @@ export class ObjectQL implements IDataEngine {
836
799
  return opCtx.result as any[];
837
800
  }
838
801
 
839
- async findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any> {
802
+ async findOne(objectName: string, query?: EngineQueryOptions): Promise<any> {
840
803
  objectName = this.resolveObjectName(objectName);
841
804
  this.logger.debug('FindOne operation', { objectName });
842
805
  const driver = this.getDriver(objectName);
843
- const ast = this.toQueryAST(objectName, query);
844
- ast.limit = 1;
806
+ const ast: QueryAST = { object: objectName, ...query, limit: 1 };
807
+ // Remove context and top alias from the AST
808
+ delete (ast as any).context;
809
+ delete (ast as any).top;
845
810
 
846
811
  const opCtx: OperationContext = {
847
812
  object: objectName,
@@ -918,16 +883,15 @@ export class ObjectQL implements IDataEngine {
918
883
  return opCtx.result;
919
884
  }
920
885
 
921
- async update(object: string, data: any, options?: DataEngineUpdateOptions): Promise<any> {
886
+ async update(object: string, data: any, options?: EngineUpdateOptions): Promise<any> {
922
887
  object = this.resolveObjectName(object);
923
888
  this.logger.debug('Update operation starting', { object });
924
889
  const driver = this.getDriver(object);
925
890
 
926
- // 1. Extract ID from data or filter if it's a single update by ID
891
+ // 1. Extract ID from data or where if it's a single update by ID
927
892
  let id = data.id;
928
- if (!id && options?.filter) {
929
- if (typeof options.filter === 'string') id = options.filter;
930
- else if (options.filter.id) id = options.filter.id;
893
+ if (!id && options?.where && typeof options.where === 'object' && 'id' in options.where) {
894
+ id = (options.where as Record<string, unknown>).id;
931
895
  }
932
896
 
933
897
  const opCtx: OperationContext = {
@@ -954,7 +918,7 @@ export class ObjectQL implements IDataEngine {
954
918
  if (hookContext.input.id) {
955
919
  result = await driver.update(object, hookContext.input.id as string, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
956
920
  } else if (options?.multi && driver.updateMany) {
957
- const ast = this.toQueryAST(object, { filter: options.filter });
921
+ const ast: QueryAST = { object, where: options.where };
958
922
  result = await driver.updateMany(object, ast, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
959
923
  } else {
960
924
  throw new Error('Update requires an ID or options.multi=true');
@@ -973,16 +937,15 @@ export class ObjectQL implements IDataEngine {
973
937
  return opCtx.result;
974
938
  }
975
939
 
976
- async delete(object: string, options?: DataEngineDeleteOptions): Promise<any> {
940
+ async delete(object: string, options?: EngineDeleteOptions): Promise<any> {
977
941
  object = this.resolveObjectName(object);
978
942
  this.logger.debug('Delete operation starting', { object });
979
943
  const driver = this.getDriver(object);
980
944
 
981
945
  // Extract ID logic similar to update
982
946
  let id: any = undefined;
983
- if (options?.filter) {
984
- if (typeof options.filter === 'string') id = options.filter;
985
- else if (options.filter.id) id = options.filter.id;
947
+ if (options?.where && typeof options.where === 'object' && 'id' in options.where) {
948
+ id = (options.where as Record<string, unknown>).id;
986
949
  }
987
950
 
988
951
  const opCtx: OperationContext = {
@@ -1008,7 +971,7 @@ export class ObjectQL implements IDataEngine {
1008
971
  if (hookContext.input.id) {
1009
972
  result = await driver.delete(object, hookContext.input.id as string, hookContext.input.options as any);
1010
973
  } else if (options?.multi && driver.deleteMany) {
1011
- const ast = this.toQueryAST(object, { filter: options.filter });
974
+ const ast: QueryAST = { object, where: options.where };
1012
975
  result = await driver.deleteMany(object, ast, hookContext.input.options as any);
1013
976
  } else {
1014
977
  throw new Error('Delete requires an ID or options.multi=true');
@@ -1027,7 +990,7 @@ export class ObjectQL implements IDataEngine {
1027
990
  return opCtx.result;
1028
991
  }
1029
992
 
1030
- async count(object: string, query?: DataEngineCountOptions): Promise<number> {
993
+ async count(object: string, query?: EngineCountOptions): Promise<number> {
1031
994
  object = this.resolveObjectName(object);
1032
995
  const driver = this.getDriver(object);
1033
996
 
@@ -1040,18 +1003,18 @@ export class ObjectQL implements IDataEngine {
1040
1003
 
1041
1004
  await this.executeWithMiddleware(opCtx, async () => {
1042
1005
  if (driver.count) {
1043
- const ast = this.toQueryAST(object, { filter: query?.filter });
1006
+ const ast: QueryAST = { object, where: query?.where };
1044
1007
  return driver.count(object, ast);
1045
1008
  }
1046
1009
  // Fallback to find().length
1047
- const res = await this.find(object, { filter: query?.filter, select: ['id'] });
1010
+ const res = await this.find(object, { where: query?.where, fields: ['id'] });
1048
1011
  return res.length;
1049
1012
  });
1050
1013
 
1051
1014
  return opCtx.result as number;
1052
1015
  }
1053
1016
 
1054
- async aggregate(object: string, query: DataEngineAggregateOptions): Promise<any[]> {
1017
+ async aggregate(object: string, query: EngineAggregateOptions): Promise<any[]> {
1055
1018
  object = this.resolveObjectName(object);
1056
1019
  const driver = this.getDriver(object);
1057
1020
  this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
@@ -1066,13 +1029,9 @@ export class ObjectQL implements IDataEngine {
1066
1029
  await this.executeWithMiddleware(opCtx, async () => {
1067
1030
  const ast: QueryAST = {
1068
1031
  object,
1069
- where: query.filter,
1032
+ where: query.where,
1070
1033
  groupBy: query.groupBy,
1071
- aggregations: query.aggregations?.map(agg => ({
1072
- function: agg.method,
1073
- field: agg.field,
1074
- alias: agg.alias || `${agg.method}_${agg.field || 'all'}`,
1075
- })),
1034
+ aggregations: query.aggregations,
1076
1035
  };
1077
1036
 
1078
1037
  return driver.find(object, ast);
@@ -1355,7 +1314,7 @@ export class ObjectRepository {
1355
1314
  /** Update a single record by ID */
1356
1315
  async updateById(id: string | number, data: any): Promise<any> {
1357
1316
  return this.engine.update(this.objectName, { ...data, id: id }, {
1358
- filter: { id: id },
1317
+ where: { id: id },
1359
1318
  context: this.context,
1360
1319
  });
1361
1320
  }
@@ -1370,7 +1329,7 @@ export class ObjectRepository {
1370
1329
  /** Delete a single record by ID */
1371
1330
  async deleteById(id: string | number): Promise<any> {
1372
1331
  return this.engine.delete(this.objectName, {
1373
- filter: { id: id },
1332
+ where: { id: id },
1374
1333
  context: this.context,
1375
1334
  });
1376
1335
  }
@@ -547,5 +547,217 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
547
547
  expect(syncedNames).not.toContain('sys__user');
548
548
  expect(syncedNames).not.toContain('sys__session');
549
549
  });
550
+
551
+ it('should use syncSchemasBatch when driver supports batchSchemaSync', async () => {
552
+ // Arrange - driver that supports batch schema sync
553
+ const batchCalls: Array<{ object: string; schema: any }[]> = [];
554
+ const singleCalls: Array<{ object: string; schema: any }> = [];
555
+ const mockDriver = {
556
+ name: 'batch-driver',
557
+ version: '1.0.0',
558
+ supports: { batchSchemaSync: true },
559
+ connect: async () => {},
560
+ disconnect: async () => {},
561
+ find: async () => [],
562
+ findOne: async () => null,
563
+ create: async (_o: string, d: any) => d,
564
+ update: async (_o: string, _i: any, d: any) => d,
565
+ delete: async () => true,
566
+ syncSchema: async (object: string, schema: any) => {
567
+ singleCalls.push({ object, schema });
568
+ },
569
+ syncSchemasBatch: async (schemas: Array<{ object: string; schema: any }>) => {
570
+ batchCalls.push(schemas);
571
+ },
572
+ };
573
+
574
+ await kernel.use({
575
+ name: 'mock-batch-driver-plugin',
576
+ type: 'driver',
577
+ version: '1.0.0',
578
+ init: async (ctx) => {
579
+ ctx.registerService('driver.batch', mockDriver);
580
+ },
581
+ });
582
+
583
+ const appManifest = {
584
+ id: 'com.test.batchapp',
585
+ name: 'batchapp',
586
+ namespace: 'bat',
587
+ version: '1.0.0',
588
+ objects: [
589
+ {
590
+ name: 'alpha',
591
+ label: 'Alpha',
592
+ fields: { a: { name: 'a', label: 'A', type: 'text' } },
593
+ },
594
+ {
595
+ name: 'beta',
596
+ label: 'Beta',
597
+ fields: { b: { name: 'b', label: 'B', type: 'text' } },
598
+ },
599
+ {
600
+ name: 'gamma',
601
+ label: 'Gamma',
602
+ fields: { c: { name: 'c', label: 'C', type: 'text' } },
603
+ },
604
+ ],
605
+ };
606
+
607
+ await kernel.use({
608
+ name: 'mock-batch-app-plugin',
609
+ type: 'app',
610
+ version: '1.0.0',
611
+ init: async (ctx) => {
612
+ ctx.registerService('app.batchapp', appManifest);
613
+ },
614
+ });
615
+
616
+ const plugin = new ObjectQLPlugin();
617
+ await kernel.use(plugin);
618
+
619
+ // Act
620
+ await kernel.bootstrap();
621
+
622
+ // Assert - syncSchemasBatch should have been called once with all objects
623
+ expect(batchCalls.length).toBe(1);
624
+ const batchedObjects = batchCalls[0].map((s) => s.object).sort();
625
+ expect(batchedObjects).toContain('bat__alpha');
626
+ expect(batchedObjects).toContain('bat__beta');
627
+ expect(batchedObjects).toContain('bat__gamma');
628
+ // syncSchema should NOT have been called individually
629
+ expect(singleCalls.length).toBe(0);
630
+ });
631
+
632
+ it('should fall back to sequential syncSchema when batch fails', async () => {
633
+ // Arrange - driver where batch fails
634
+ const singleCalls: Array<{ object: string; schema: any }> = [];
635
+ const mockDriver = {
636
+ name: 'fallback-driver',
637
+ version: '1.0.0',
638
+ supports: { batchSchemaSync: true },
639
+ connect: async () => {},
640
+ disconnect: async () => {},
641
+ find: async () => [],
642
+ findOne: async () => null,
643
+ create: async (_o: string, d: any) => d,
644
+ update: async (_o: string, _i: any, d: any) => d,
645
+ delete: async () => true,
646
+ syncSchema: async (object: string, schema: any) => {
647
+ singleCalls.push({ object, schema });
648
+ },
649
+ syncSchemasBatch: async () => {
650
+ throw new Error('batch not supported at runtime');
651
+ },
652
+ };
653
+
654
+ await kernel.use({
655
+ name: 'mock-fallback-driver-plugin',
656
+ type: 'driver',
657
+ version: '1.0.0',
658
+ init: async (ctx) => {
659
+ ctx.registerService('driver.fallback', mockDriver);
660
+ },
661
+ });
662
+
663
+ const appManifest = {
664
+ id: 'com.test.fallback',
665
+ name: 'fallback',
666
+ namespace: 'fb',
667
+ version: '1.0.0',
668
+ objects: [
669
+ {
670
+ name: 'one',
671
+ label: 'One',
672
+ fields: { x: { name: 'x', label: 'X', type: 'text' } },
673
+ },
674
+ {
675
+ name: 'two',
676
+ label: 'Two',
677
+ fields: { y: { name: 'y', label: 'Y', type: 'text' } },
678
+ },
679
+ ],
680
+ };
681
+
682
+ await kernel.use({
683
+ name: 'mock-fallback-app-plugin',
684
+ type: 'app',
685
+ version: '1.0.0',
686
+ init: async (ctx) => {
687
+ ctx.registerService('app.fallback', appManifest);
688
+ },
689
+ });
690
+
691
+ const plugin = new ObjectQLPlugin();
692
+ await kernel.use(plugin);
693
+
694
+ // Act - should not throw
695
+ await expect(kernel.bootstrap()).resolves.not.toThrow();
696
+
697
+ // Assert - sequential fallback should have been used
698
+ const syncedObjects = singleCalls.map((s) => s.object).sort();
699
+ expect(syncedObjects).toContain('fb__one');
700
+ expect(syncedObjects).toContain('fb__two');
701
+ });
702
+
703
+ it('should not use batch when driver does not support batchSchemaSync', async () => {
704
+ // Arrange - driver without batch support (but with syncSchema)
705
+ const singleCalls: string[] = [];
706
+ const mockDriver = {
707
+ name: 'nobatch-driver',
708
+ version: '1.0.0',
709
+ connect: async () => {},
710
+ disconnect: async () => {},
711
+ find: async () => [],
712
+ findOne: async () => null,
713
+ create: async (_o: string, d: any) => d,
714
+ update: async (_o: string, _i: any, d: any) => d,
715
+ delete: async () => true,
716
+ syncSchema: async (object: string) => {
717
+ singleCalls.push(object);
718
+ },
719
+ };
720
+
721
+ await kernel.use({
722
+ name: 'mock-nobatch-driver-plugin',
723
+ type: 'driver',
724
+ version: '1.0.0',
725
+ init: async (ctx) => {
726
+ ctx.registerService('driver.nobatch', mockDriver);
727
+ },
728
+ });
729
+
730
+ const appManifest = {
731
+ id: 'com.test.nobatch',
732
+ name: 'nobatch',
733
+ namespace: 'nb',
734
+ version: '1.0.0',
735
+ objects: [
736
+ {
737
+ name: 'item',
738
+ label: 'Item',
739
+ fields: { z: { name: 'z', label: 'Z', type: 'text' } },
740
+ },
741
+ ],
742
+ };
743
+
744
+ await kernel.use({
745
+ name: 'mock-nobatch-app-plugin',
746
+ type: 'app',
747
+ version: '1.0.0',
748
+ init: async (ctx) => {
749
+ ctx.registerService('app.nobatch', appManifest);
750
+ },
751
+ });
752
+
753
+ const plugin = new ObjectQLPlugin();
754
+ await kernel.use(plugin);
755
+
756
+ // Act
757
+ await kernel.bootstrap();
758
+
759
+ // Assert - sequential syncSchema should have been used
760
+ expect(singleCalls).toContain('nb__item');
761
+ });
550
762
  });
551
763
  });