@objectstack/objectql 3.3.0 → 3.3.1
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 +8 -0
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +61 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +61 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/plugin.integration.test.ts +212 -0
- package/src/plugin.ts +71 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/objectql",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
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.3.
|
|
17
|
-
"@objectstack/spec": "3.3.
|
|
18
|
-
"@objectstack/types": "3.3.
|
|
16
|
+
"@objectstack/core": "3.3.1",
|
|
17
|
+
"@objectstack/spec": "3.3.1",
|
|
18
|
+
"@objectstack/types": "3.3.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"typescript": "^5.0.0",
|
|
@@ -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
|
});
|
package/src/plugin.ts
CHANGED
|
@@ -239,9 +239,13 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
239
239
|
/**
|
|
240
240
|
* Synchronize all registered object schemas to the database.
|
|
241
241
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
242
|
+
* Groups objects by their responsible driver, then:
|
|
243
|
+
* - If the driver advertises `supports.batchSchemaSync` and implements
|
|
244
|
+
* `syncSchemasBatch()`, submits all schemas in a single call (reducing
|
|
245
|
+
* network round-trips for remote drivers like Turso).
|
|
246
|
+
* - Otherwise falls back to sequential `syncSchema()` per object.
|
|
247
|
+
*
|
|
248
|
+
* This is idempotent — drivers must tolerate repeated calls without
|
|
245
249
|
* duplicating tables or erroring out.
|
|
246
250
|
*
|
|
247
251
|
* Drivers that do not implement `syncSchema` are silently skipped.
|
|
@@ -255,6 +259,9 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
255
259
|
let synced = 0;
|
|
256
260
|
let skipped = 0;
|
|
257
261
|
|
|
262
|
+
// Group objects by driver for potential batch optimization
|
|
263
|
+
const driverGroups = new Map<any, Array<{ obj: any; tableName: string }>>();
|
|
264
|
+
|
|
258
265
|
for (const obj of allObjects) {
|
|
259
266
|
const driver = this.ql.getDriverForObject(obj.name);
|
|
260
267
|
if (!driver) {
|
|
@@ -274,21 +281,69 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
274
281
|
continue;
|
|
275
282
|
}
|
|
276
283
|
|
|
277
|
-
// Use the physical table name (e.g., 'sys_user') for DDL operations
|
|
278
|
-
// instead of the FQN (e.g., 'sys__user'). ObjectSchema.create()
|
|
279
|
-
// auto-derives tableName as {namespace}_{name}.
|
|
280
284
|
const tableName = obj.tableName || obj.name;
|
|
281
285
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
let group = driverGroups.get(driver);
|
|
287
|
+
if (!group) {
|
|
288
|
+
group = [];
|
|
289
|
+
driverGroups.set(driver, group);
|
|
290
|
+
}
|
|
291
|
+
group.push({ obj, tableName });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Process each driver group
|
|
295
|
+
for (const [driver, entries] of driverGroups) {
|
|
296
|
+
// Batch path: driver supports batch schema sync
|
|
297
|
+
if (
|
|
298
|
+
driver.supports?.batchSchemaSync &&
|
|
299
|
+
typeof driver.syncSchemasBatch === 'function'
|
|
300
|
+
) {
|
|
301
|
+
const batchPayload = entries.map((e) => ({
|
|
302
|
+
object: e.tableName,
|
|
303
|
+
schema: e.obj,
|
|
304
|
+
}));
|
|
305
|
+
try {
|
|
306
|
+
await driver.syncSchemasBatch(batchPayload);
|
|
307
|
+
synced += entries.length;
|
|
308
|
+
ctx.logger.debug('Batch schema sync succeeded', {
|
|
309
|
+
driver: driver.name,
|
|
310
|
+
count: entries.length,
|
|
311
|
+
});
|
|
312
|
+
} catch (e: unknown) {
|
|
313
|
+
ctx.logger.warn('Batch schema sync failed, falling back to sequential', {
|
|
314
|
+
driver: driver.name,
|
|
315
|
+
error: e instanceof Error ? e.message : String(e),
|
|
316
|
+
});
|
|
317
|
+
// Fallback: sequential sync for this driver's objects
|
|
318
|
+
for (const { obj, tableName } of entries) {
|
|
319
|
+
try {
|
|
320
|
+
await driver.syncSchema(tableName, obj);
|
|
321
|
+
synced++;
|
|
322
|
+
} catch (seqErr: unknown) {
|
|
323
|
+
ctx.logger.warn('Failed to sync schema for object', {
|
|
324
|
+
object: obj.name,
|
|
325
|
+
tableName,
|
|
326
|
+
driver: driver.name,
|
|
327
|
+
error: seqErr instanceof Error ? seqErr.message : String(seqErr),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
// Sequential path: no batch support
|
|
334
|
+
for (const { obj, tableName } of entries) {
|
|
335
|
+
try {
|
|
336
|
+
await driver.syncSchema(tableName, obj);
|
|
337
|
+
synced++;
|
|
338
|
+
} catch (e: unknown) {
|
|
339
|
+
ctx.logger.warn('Failed to sync schema for object', {
|
|
340
|
+
object: obj.name,
|
|
341
|
+
tableName,
|
|
342
|
+
driver: driver.name,
|
|
343
|
+
error: e instanceof Error ? e.message : String(e),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
292
347
|
}
|
|
293
348
|
}
|
|
294
349
|
|