@objectstack/objectql 3.2.9 → 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 +16 -0
- package/dist/index.d.mts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +77 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +77 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/engine.test.ts +5 -5
- package/src/engine.ts +7 -4
- package/src/plugin.integration.test.ts +291 -0
- package/src/plugin.ts +73 -12
- package/src/registry.test.ts +25 -0
- package/src/registry.ts +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/objectql",
|
|
3
|
-
"version": "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.
|
|
17
|
-
"@objectstack/spec": "3.
|
|
18
|
-
"@objectstack/types": "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",
|
package/src/engine.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { ObjectQL } from './engine';
|
|
3
3
|
import { SchemaRegistry } from './registry';
|
|
4
|
-
import {
|
|
4
|
+
import type { IDataDriver } from '@objectstack/spec/contracts';
|
|
5
5
|
|
|
6
6
|
// Mock the SchemaRegistry to avoid side effects between tests
|
|
7
7
|
vi.mock('./registry', () => {
|
|
@@ -35,8 +35,8 @@ vi.mock('./registry', () => {
|
|
|
35
35
|
|
|
36
36
|
describe('ObjectQL Engine', () => {
|
|
37
37
|
let engine: ObjectQL;
|
|
38
|
-
let mockDriver:
|
|
39
|
-
let mockDriver2:
|
|
38
|
+
let mockDriver: IDataDriver;
|
|
39
|
+
let mockDriver2: IDataDriver;
|
|
40
40
|
|
|
41
41
|
beforeEach(() => {
|
|
42
42
|
// Clear Registry Mocks
|
|
@@ -54,7 +54,7 @@ describe('ObjectQL Engine', () => {
|
|
|
54
54
|
delete: vi.fn(),
|
|
55
55
|
count: vi.fn(),
|
|
56
56
|
capabilities: {} as any // Simplified
|
|
57
|
-
} as unknown as
|
|
57
|
+
} as unknown as IDataDriver;
|
|
58
58
|
|
|
59
59
|
mockDriver2 = {
|
|
60
60
|
name: 'mongo',
|
|
@@ -67,7 +67,7 @@ describe('ObjectQL Engine', () => {
|
|
|
67
67
|
delete: vi.fn(),
|
|
68
68
|
count: vi.fn(),
|
|
69
69
|
capabilities: {} as any
|
|
70
|
-
} as unknown as
|
|
70
|
+
} as unknown as IDataDriver;
|
|
71
71
|
|
|
72
72
|
engine = new ObjectQL();
|
|
73
73
|
});
|
package/src/engine.ts
CHANGED
|
@@ -545,7 +545,10 @@ export class ObjectQL implements IDataEngine {
|
|
|
545
545
|
private resolveObjectName(name: string): string {
|
|
546
546
|
const schema = SchemaRegistry.getObject(name);
|
|
547
547
|
if (schema) {
|
|
548
|
-
|
|
548
|
+
// Prefer the physical table name (e.g., 'sys_user') over the FQN
|
|
549
|
+
// (e.g., 'sys__user'). ObjectSchema.create() auto-derives tableName
|
|
550
|
+
// as {namespace}_{name} which matches the storage convention.
|
|
551
|
+
return schema.tableName || schema.name;
|
|
549
552
|
}
|
|
550
553
|
return name; // Ad-hoc object, keep as-is
|
|
551
554
|
}
|
|
@@ -898,7 +901,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
898
901
|
result = await Promise.all((hookContext.input.data as any[]).map((item: any) => driver.create(object, item, hookContext.input.options as any)));
|
|
899
902
|
}
|
|
900
903
|
} else {
|
|
901
|
-
result = await driver.create(object, hookContext.input.data, hookContext.input.options as any);
|
|
904
|
+
result = await driver.create(object, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
|
|
902
905
|
}
|
|
903
906
|
|
|
904
907
|
hookContext.event = 'afterInsert';
|
|
@@ -949,10 +952,10 @@ export class ObjectQL implements IDataEngine {
|
|
|
949
952
|
try {
|
|
950
953
|
let result;
|
|
951
954
|
if (hookContext.input.id) {
|
|
952
|
-
result = await driver.update(object, hookContext.input.id as string, hookContext.input.data, hookContext.input.options as any);
|
|
955
|
+
result = await driver.update(object, hookContext.input.id as string, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
|
|
953
956
|
} else if (options?.multi && driver.updateMany) {
|
|
954
957
|
const ast = this.toQueryAST(object, { filter: options.filter });
|
|
955
|
-
result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options as any);
|
|
958
|
+
result = await driver.updateMany(object, ast, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
|
|
956
959
|
} else {
|
|
957
960
|
throw new Error('Update requires an ID or options.multi=true');
|
|
958
961
|
}
|
|
@@ -468,5 +468,296 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
|
|
|
468
468
|
// Act & Assert - should not throw
|
|
469
469
|
await expect(kernel.bootstrap()).resolves.not.toThrow();
|
|
470
470
|
});
|
|
471
|
+
|
|
472
|
+
it('should use tableName for syncSchema when objects have auto-derived tableName', async () => {
|
|
473
|
+
// Arrange - driver that tracks syncSchema calls
|
|
474
|
+
const synced: Array<{ object: string; schema: any }> = [];
|
|
475
|
+
const mockDriver = {
|
|
476
|
+
name: 'table-name-driver',
|
|
477
|
+
version: '1.0.0',
|
|
478
|
+
connect: async () => {},
|
|
479
|
+
disconnect: async () => {},
|
|
480
|
+
find: async () => [],
|
|
481
|
+
findOne: async () => null,
|
|
482
|
+
create: async (_o: string, d: any) => d,
|
|
483
|
+
update: async (_o: string, _i: any, d: any) => d,
|
|
484
|
+
delete: async () => true,
|
|
485
|
+
syncSchema: async (object: string, schema: any) => {
|
|
486
|
+
synced.push({ object, schema });
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
await kernel.use({
|
|
491
|
+
name: 'mock-driver-plugin',
|
|
492
|
+
type: 'driver',
|
|
493
|
+
version: '1.0.0',
|
|
494
|
+
init: async (ctx) => {
|
|
495
|
+
ctx.registerService('driver.table-name', mockDriver);
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Objects with tableName (simulating ObjectSchema.create() output)
|
|
500
|
+
const appManifest = {
|
|
501
|
+
id: 'com.test.system',
|
|
502
|
+
name: 'system',
|
|
503
|
+
namespace: 'sys',
|
|
504
|
+
version: '1.0.0',
|
|
505
|
+
objects: [
|
|
506
|
+
{
|
|
507
|
+
name: 'user',
|
|
508
|
+
label: 'User',
|
|
509
|
+
namespace: 'sys',
|
|
510
|
+
tableName: 'sys_user',
|
|
511
|
+
fields: {
|
|
512
|
+
email: { name: 'email', label: 'Email', type: 'text' },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'session',
|
|
517
|
+
label: 'Session',
|
|
518
|
+
namespace: 'sys',
|
|
519
|
+
tableName: 'sys_session',
|
|
520
|
+
fields: {
|
|
521
|
+
token: { name: 'token', label: 'Token', type: 'text' },
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
await kernel.use({
|
|
528
|
+
name: 'mock-app-plugin',
|
|
529
|
+
type: 'app',
|
|
530
|
+
version: '1.0.0',
|
|
531
|
+
init: async (ctx) => {
|
|
532
|
+
ctx.registerService('app.system', appManifest);
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const plugin = new ObjectQLPlugin();
|
|
537
|
+
await kernel.use(plugin);
|
|
538
|
+
|
|
539
|
+
// Act
|
|
540
|
+
await kernel.bootstrap();
|
|
541
|
+
|
|
542
|
+
// Assert - syncSchema should use tableName (single underscore) not FQN (double underscore)
|
|
543
|
+
const syncedNames = synced.map((s) => s.object).sort();
|
|
544
|
+
expect(syncedNames).toContain('sys_user');
|
|
545
|
+
expect(syncedNames).toContain('sys_session');
|
|
546
|
+
// Should NOT contain double-underscore FQN
|
|
547
|
+
expect(syncedNames).not.toContain('sys__user');
|
|
548
|
+
expect(syncedNames).not.toContain('sys__session');
|
|
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
|
+
});
|
|
471
762
|
});
|
|
472
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,15 +281,69 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
274
281
|
continue;
|
|
275
282
|
}
|
|
276
283
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
const tableName = obj.tableName || obj.name;
|
|
285
|
+
|
|
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
|
+
}
|
|
286
347
|
}
|
|
287
348
|
}
|
|
288
349
|
|
package/src/registry.test.ts
CHANGED
|
@@ -187,6 +187,31 @@ describe('SchemaRegistry', () => {
|
|
|
187
187
|
expect(SchemaRegistry.getObject('task')).toBeDefined();
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
it('should resolve by tableName (protocol name fallback)', () => {
|
|
191
|
+
// Simulates ObjectSchema.create() which auto-derives tableName
|
|
192
|
+
// as {namespace}_{name} (single underscore)
|
|
193
|
+
const obj = { name: 'user', tableName: 'sys_user', namespace: 'sys', fields: {} };
|
|
194
|
+
SchemaRegistry.registerObject(obj as any, 'com.objectstack.system', 'sys', 'own');
|
|
195
|
+
|
|
196
|
+
// FQN is 'sys__user' (double underscore)
|
|
197
|
+
expect(SchemaRegistry.getObject('sys__user')).toBeDefined();
|
|
198
|
+
|
|
199
|
+
// Protocol name 'sys_user' (single underscore) should also resolve
|
|
200
|
+
const resolved = SchemaRegistry.getObject('sys_user');
|
|
201
|
+
expect(resolved).toBeDefined();
|
|
202
|
+
expect(resolved?.name).toBe('sys__user');
|
|
203
|
+
expect((resolved as any).tableName).toBe('sys_user');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should resolve by tableName for any namespace', () => {
|
|
207
|
+
const obj = { name: 'account', tableName: 'crm_account', namespace: 'crm', fields: {} };
|
|
208
|
+
SchemaRegistry.registerObject(obj as any, 'com.crm', 'crm', 'own');
|
|
209
|
+
|
|
210
|
+
// FQN: 'crm__account', tableName: 'crm_account'
|
|
211
|
+
expect(SchemaRegistry.getObject('crm__account')).toBeDefined();
|
|
212
|
+
expect(SchemaRegistry.getObject('crm_account')).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
190
215
|
it('should cache merged objects', () => {
|
|
191
216
|
const obj = { name: 'cached', fields: {} };
|
|
192
217
|
SchemaRegistry.registerObject(obj as any, 'com.test', 'test', 'own');
|
package/src/registry.ts
CHANGED
|
@@ -314,8 +314,14 @@ export class SchemaRegistry {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/**
|
|
317
|
-
* Get object by name (FQN
|
|
318
|
-
*
|
|
317
|
+
* Get object by name (FQN, short name, or physical table name).
|
|
318
|
+
*
|
|
319
|
+
* Resolution order:
|
|
320
|
+
* 1. Exact FQN match (e.g., 'crm__account')
|
|
321
|
+
* 2. Short name fallback (e.g., 'account' → 'crm__account')
|
|
322
|
+
* 3. Physical table name match (e.g., 'sys_user' → 'sys__user')
|
|
323
|
+
* ObjectSchema.create() auto-derives tableName as {namespace}_{name},
|
|
324
|
+
* which uses a single underscore — different from the FQN double underscore.
|
|
319
325
|
*/
|
|
320
326
|
static getObject(name: string): ServiceObject | undefined {
|
|
321
327
|
// Direct FQN lookup
|
|
@@ -331,6 +337,15 @@ export class SchemaRegistry {
|
|
|
331
337
|
}
|
|
332
338
|
}
|
|
333
339
|
|
|
340
|
+
// Fallback: match by physical table name (e.g., 'sys_user' → FQN 'sys__user')
|
|
341
|
+
// This bridges the gap between protocol names (SystemObjectName) and FQN.
|
|
342
|
+
for (const fqn of this.objectContributors.keys()) {
|
|
343
|
+
const resolved = this.resolveObject(fqn);
|
|
344
|
+
if (resolved?.tableName === name) {
|
|
345
|
+
return resolved;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
334
349
|
return undefined;
|
|
335
350
|
}
|
|
336
351
|
|