@objectstack/objectql 4.0.3 → 4.0.4
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 +9 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +94 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +94 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/datasource-mapping.test.ts +181 -0
- package/src/engine.test.ts +15 -1
- package/src/engine.ts +137 -17
- package/src/protocol.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/objectql",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
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": "4.0.
|
|
17
|
-
"@objectstack/spec": "4.0.
|
|
18
|
-
"@objectstack/types": "4.0.
|
|
16
|
+
"@objectstack/core": "4.0.4",
|
|
17
|
+
"@objectstack/spec": "4.0.4",
|
|
18
|
+
"@objectstack/types": "4.0.4"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"typescript": "^6.0.2",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { ObjectQL } from './engine.js';
|
|
5
|
+
import { SchemaRegistry } from './registry.js';
|
|
6
|
+
|
|
7
|
+
// Mock driver for testing
|
|
8
|
+
const createMockDriver = (name: string) => ({
|
|
9
|
+
name,
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
supports: {},
|
|
12
|
+
connect: async () => {},
|
|
13
|
+
disconnect: async () => {},
|
|
14
|
+
checkHealth: async () => true,
|
|
15
|
+
find: async () => [],
|
|
16
|
+
findOne: async () => null,
|
|
17
|
+
create: async (obj: string, data: any) => ({ id: '1', ...data }),
|
|
18
|
+
update: async (obj: string, id: string, data: any) => ({ id, ...data }),
|
|
19
|
+
delete: async () => true,
|
|
20
|
+
count: async () => 0,
|
|
21
|
+
bulkCreate: async () => [],
|
|
22
|
+
bulkUpdate: async () => [],
|
|
23
|
+
bulkDelete: async () => {},
|
|
24
|
+
execute: async () => ({}),
|
|
25
|
+
findStream: async function* () {},
|
|
26
|
+
upsert: async (obj: string, data: any) => ({ id: '1', ...data }),
|
|
27
|
+
beginTransaction: async () => ({}),
|
|
28
|
+
commit: async () => {},
|
|
29
|
+
rollback: async () => {},
|
|
30
|
+
syncSchema: async () => {},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('DatasourceMapping', () => {
|
|
34
|
+
let engine: ObjectQL;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
engine = new ObjectQL();
|
|
38
|
+
SchemaRegistry.reset();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should route objects by namespace', async () => {
|
|
42
|
+
const memoryDriver = createMockDriver('memory');
|
|
43
|
+
const tursoDriver = createMockDriver('turso');
|
|
44
|
+
|
|
45
|
+
engine.registerDriver(memoryDriver);
|
|
46
|
+
engine.registerDriver(tursoDriver, true); // default
|
|
47
|
+
|
|
48
|
+
// Configure mapping: crm namespace → memory
|
|
49
|
+
engine.setDatasourceMapping([
|
|
50
|
+
{ namespace: 'crm', datasource: 'memory' },
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// Register an object in crm namespace
|
|
54
|
+
SchemaRegistry.registerObject(
|
|
55
|
+
{
|
|
56
|
+
name: 'account',
|
|
57
|
+
fields: { name: { type: 'text' } },
|
|
58
|
+
},
|
|
59
|
+
'com.example.crm',
|
|
60
|
+
'crm',
|
|
61
|
+
'own'
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Test that it uses memory driver
|
|
65
|
+
const result = await engine.insert('account', { name: 'Test Account' });
|
|
66
|
+
expect(result).toBeDefined();
|
|
67
|
+
expect(result.name).toBe('Test Account');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should route objects by pattern', async () => {
|
|
71
|
+
const memoryDriver = createMockDriver('memory');
|
|
72
|
+
const tursoDriver = createMockDriver('turso');
|
|
73
|
+
|
|
74
|
+
engine.registerDriver(memoryDriver);
|
|
75
|
+
engine.registerDriver(tursoDriver, true);
|
|
76
|
+
|
|
77
|
+
// Configure mapping: sys_* pattern → turso
|
|
78
|
+
engine.setDatasourceMapping([
|
|
79
|
+
{ objectPattern: 'sys_*', datasource: 'turso' },
|
|
80
|
+
{ default: true, datasource: 'memory' },
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
// Register system objects
|
|
84
|
+
SchemaRegistry.registerObject(
|
|
85
|
+
{
|
|
86
|
+
name: 'sys_user',
|
|
87
|
+
fields: { username: { type: 'text' } },
|
|
88
|
+
},
|
|
89
|
+
'com.objectstack.system',
|
|
90
|
+
'system',
|
|
91
|
+
'own'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = await engine.insert('sys_user', { username: 'admin' });
|
|
95
|
+
expect(result).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should respect priority order', async () => {
|
|
99
|
+
const memoryDriver = createMockDriver('memory');
|
|
100
|
+
const tursoDriver = createMockDriver('turso');
|
|
101
|
+
|
|
102
|
+
engine.registerDriver(memoryDriver);
|
|
103
|
+
engine.registerDriver(tursoDriver);
|
|
104
|
+
|
|
105
|
+
// Higher priority rule should win
|
|
106
|
+
engine.setDatasourceMapping([
|
|
107
|
+
{ namespace: 'crm', datasource: 'memory', priority: 100 },
|
|
108
|
+
{ namespace: 'crm', datasource: 'turso', priority: 50 }, // Lower number = higher priority
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
SchemaRegistry.registerObject(
|
|
112
|
+
{
|
|
113
|
+
name: 'account',
|
|
114
|
+
fields: { name: { type: 'text' } },
|
|
115
|
+
},
|
|
116
|
+
'com.example.crm',
|
|
117
|
+
'crm',
|
|
118
|
+
'own'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Should use turso (priority 50) not memory (priority 100)
|
|
122
|
+
const result = await engine.insert('account', { name: 'Test' });
|
|
123
|
+
expect(result).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should fallback to default rule', async () => {
|
|
127
|
+
const memoryDriver = createMockDriver('memory');
|
|
128
|
+
const tursoDriver = createMockDriver('turso');
|
|
129
|
+
|
|
130
|
+
engine.registerDriver(memoryDriver);
|
|
131
|
+
engine.registerDriver(tursoDriver);
|
|
132
|
+
|
|
133
|
+
engine.setDatasourceMapping([
|
|
134
|
+
{ namespace: 'auth', datasource: 'turso' },
|
|
135
|
+
{ default: true, datasource: 'memory' },
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Register object in different namespace
|
|
139
|
+
SchemaRegistry.registerObject(
|
|
140
|
+
{
|
|
141
|
+
name: 'task',
|
|
142
|
+
fields: { title: { type: 'text' } },
|
|
143
|
+
},
|
|
144
|
+
'com.example.todo',
|
|
145
|
+
'todo',
|
|
146
|
+
'own'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Should use memory (default)
|
|
150
|
+
const result = await engine.insert('task', { title: 'Do something' });
|
|
151
|
+
expect(result).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should prefer object explicit datasource over mapping', async () => {
|
|
155
|
+
const memoryDriver = createMockDriver('memory');
|
|
156
|
+
const tursoDriver = createMockDriver('turso');
|
|
157
|
+
|
|
158
|
+
engine.registerDriver(memoryDriver);
|
|
159
|
+
engine.registerDriver(tursoDriver);
|
|
160
|
+
|
|
161
|
+
engine.setDatasourceMapping([
|
|
162
|
+
{ namespace: 'crm', datasource: 'memory' },
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
// Object explicitly sets datasource
|
|
166
|
+
SchemaRegistry.registerObject(
|
|
167
|
+
{
|
|
168
|
+
name: 'account',
|
|
169
|
+
datasource: 'turso', // Explicit override
|
|
170
|
+
fields: { name: { type: 'text' } },
|
|
171
|
+
},
|
|
172
|
+
'com.example.crm',
|
|
173
|
+
'crm',
|
|
174
|
+
'own'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Should use turso (explicit) not memory (mapping)
|
|
178
|
+
const result = await engine.insert('account', { name: 'Test' });
|
|
179
|
+
expect(result).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
package/src/engine.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { IDataDriver } from '@objectstack/spec/contracts';
|
|
|
6
6
|
// Mock the SchemaRegistry to avoid side effects between tests
|
|
7
7
|
vi.mock('./registry', () => {
|
|
8
8
|
const mockObjects = new Map();
|
|
9
|
+
const mockContributors = new Map();
|
|
9
10
|
return {
|
|
10
11
|
SchemaRegistry: {
|
|
11
12
|
getObject: vi.fn((name) => mockObjects.get(name)),
|
|
@@ -13,8 +14,18 @@ vi.mock('./registry', () => {
|
|
|
13
14
|
registerObject: vi.fn((obj, packageId, namespace, ownership, priority) => {
|
|
14
15
|
const fqn = namespace ? `${namespace}__${obj.name}` : obj.name;
|
|
15
16
|
mockObjects.set(fqn, { ...obj, name: fqn });
|
|
17
|
+
// Also track contributors for getObjectOwner
|
|
18
|
+
if (!mockContributors.has(fqn)) {
|
|
19
|
+
mockContributors.set(fqn, []);
|
|
20
|
+
}
|
|
21
|
+
const contributors = mockContributors.get(fqn);
|
|
22
|
+
contributors.push({ packageId, namespace, ownership, priority, definition: obj });
|
|
16
23
|
return fqn;
|
|
17
24
|
}),
|
|
25
|
+
getObjectOwner: vi.fn((fqn) => {
|
|
26
|
+
const contributors = mockContributors.get(fqn);
|
|
27
|
+
return contributors?.find(c => c.ownership === 'own');
|
|
28
|
+
}),
|
|
18
29
|
registerNamespace: vi.fn(),
|
|
19
30
|
registerKind: vi.fn(),
|
|
20
31
|
registerItem: vi.fn(),
|
|
@@ -25,7 +36,10 @@ vi.mock('./registry', () => {
|
|
|
25
36
|
enabled: true,
|
|
26
37
|
installedAt: new Date().toISOString(),
|
|
27
38
|
})),
|
|
28
|
-
reset: vi.fn(() =>
|
|
39
|
+
reset: vi.fn(() => {
|
|
40
|
+
mockObjects.clear();
|
|
41
|
+
mockContributors.clear();
|
|
42
|
+
}),
|
|
29
43
|
metadata: {
|
|
30
44
|
get: vi.fn(() => mockObjects) // Expose for verification if needed
|
|
31
45
|
}
|
package/src/engine.ts
CHANGED
|
@@ -72,6 +72,19 @@ export class ObjectQL implements IDataEngine {
|
|
|
72
72
|
private defaultDriver: string | null = null;
|
|
73
73
|
private logger: Logger;
|
|
74
74
|
|
|
75
|
+
// Datasource mapping rules (imported from defineStack)
|
|
76
|
+
private datasourceMapping: Array<{
|
|
77
|
+
namespace?: string;
|
|
78
|
+
package?: string;
|
|
79
|
+
objectPattern?: string;
|
|
80
|
+
default?: boolean;
|
|
81
|
+
datasource: string;
|
|
82
|
+
priority?: number;
|
|
83
|
+
}> = [];
|
|
84
|
+
|
|
85
|
+
// Package manifests registry (for defaultDatasource lookup)
|
|
86
|
+
private manifests = new Map<string, any>();
|
|
87
|
+
|
|
75
88
|
// Per-object hooks with priority support
|
|
76
89
|
private hooks: Map<string, HookEntry[]> = new Map([
|
|
77
90
|
['beforeFind', []], ['afterFind', []],
|
|
@@ -308,6 +321,11 @@ export class ObjectQL implements IDataEngine {
|
|
|
308
321
|
const namespace = manifest.namespace as string | undefined;
|
|
309
322
|
this.logger.debug('Registering package manifest', { id, namespace });
|
|
310
323
|
|
|
324
|
+
// Store manifest for defaultDatasource lookup
|
|
325
|
+
if (id) {
|
|
326
|
+
this.manifests.set(id, manifest);
|
|
327
|
+
}
|
|
328
|
+
|
|
311
329
|
// 1. Register the Package (manifest + lifecycle state)
|
|
312
330
|
SchemaRegistry.installPackage(manifest);
|
|
313
331
|
this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace });
|
|
@@ -571,36 +589,138 @@ export class ObjectQL implements IDataEngine {
|
|
|
571
589
|
|
|
572
590
|
/**
|
|
573
591
|
* Helper to get the target driver
|
|
592
|
+
*
|
|
593
|
+
* Resolution priority (first match wins):
|
|
594
|
+
* 1. Object's explicit `datasource` field (if not 'default')
|
|
595
|
+
* 2. DatasourceMapping rules (namespace/package/pattern matching)
|
|
596
|
+
* 3. Package's `defaultDatasource` from manifest
|
|
597
|
+
* 4. Global default driver
|
|
574
598
|
*/
|
|
575
599
|
private getDriver(objectName: string): DriverInterface {
|
|
576
600
|
const object = SchemaRegistry.getObject(objectName);
|
|
577
|
-
|
|
578
|
-
// 1.
|
|
579
|
-
if (object) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
601
|
+
|
|
602
|
+
// 1. Object's explicit datasource field (highest priority)
|
|
603
|
+
if (object?.datasource && object.datasource !== 'default') {
|
|
604
|
+
if (this.drivers.has(object.datasource)) {
|
|
605
|
+
return this.drivers.get(object.datasource)!;
|
|
606
|
+
}
|
|
607
|
+
throw new Error(`[ObjectQL] Datasource '${object.datasource}' configured for object '${objectName}' is not registered.`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 2. Check datasourceMapping rules
|
|
611
|
+
const mappedDatasource = this.resolveDatasourceFromMapping(objectName, object);
|
|
612
|
+
if (mappedDatasource && this.drivers.has(mappedDatasource)) {
|
|
613
|
+
this.logger.debug('Resolved datasource from mapping', {
|
|
614
|
+
object: objectName,
|
|
615
|
+
datasource: mappedDatasource
|
|
616
|
+
});
|
|
617
|
+
return this.drivers.get(mappedDatasource)!;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 3. Check package's defaultDatasource
|
|
621
|
+
// Use the object's FQN name (from getObject) for ownership lookup
|
|
622
|
+
const fqn = object?.name || objectName;
|
|
623
|
+
const owner = SchemaRegistry.getObjectOwner(fqn);
|
|
624
|
+
if (owner?.packageId) {
|
|
625
|
+
const manifest = this.manifests.get(owner.packageId);
|
|
626
|
+
if (manifest?.defaultDatasource && manifest.defaultDatasource !== 'default') {
|
|
627
|
+
if (this.drivers.has(manifest.defaultDatasource)) {
|
|
628
|
+
this.logger.debug('Resolved datasource from package manifest', {
|
|
629
|
+
object: objectName,
|
|
630
|
+
package: owner.packageId,
|
|
631
|
+
datasource: manifest.defaultDatasource
|
|
632
|
+
});
|
|
633
|
+
return this.drivers.get(manifest.defaultDatasource)!;
|
|
591
634
|
}
|
|
592
|
-
throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
|
|
593
635
|
}
|
|
594
636
|
}
|
|
595
637
|
|
|
596
|
-
//
|
|
597
|
-
if (this.defaultDriver) {
|
|
638
|
+
// 4. Fallback to global default driver
|
|
639
|
+
if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
598
640
|
return this.drivers.get(this.defaultDriver)!;
|
|
599
641
|
}
|
|
600
642
|
|
|
601
643
|
throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
|
|
602
644
|
}
|
|
603
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Resolve datasource from mapping rules
|
|
648
|
+
*
|
|
649
|
+
* Rules are evaluated in order (or by priority if specified).
|
|
650
|
+
* First matching rule wins.
|
|
651
|
+
*/
|
|
652
|
+
private resolveDatasourceFromMapping(
|
|
653
|
+
objectName: string,
|
|
654
|
+
object?: any
|
|
655
|
+
): string | null {
|
|
656
|
+
if (!this.datasourceMapping || this.datasourceMapping.length === 0) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Sort rules by priority if any have priority set
|
|
661
|
+
const sortedRules = [...this.datasourceMapping].sort((a, b) => {
|
|
662
|
+
const aPriority = a.priority ?? 1000;
|
|
663
|
+
const bPriority = b.priority ?? 1000;
|
|
664
|
+
return aPriority - bPriority;
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
for (const rule of sortedRules) {
|
|
668
|
+
// 1. Match by namespace
|
|
669
|
+
if (rule.namespace && object?.namespace === rule.namespace) {
|
|
670
|
+
return rule.datasource;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// 2. Match by package ID
|
|
674
|
+
if (rule.package && object?.packageId === rule.package) {
|
|
675
|
+
return rule.datasource;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 3. Match by object name pattern (glob-style)
|
|
679
|
+
if (rule.objectPattern && this.matchPattern(objectName, rule.objectPattern)) {
|
|
680
|
+
return rule.datasource;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 4. Default fallback rule
|
|
684
|
+
if (rule.default) {
|
|
685
|
+
return rule.datasource;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Simple glob pattern matching
|
|
694
|
+
* Supports * (any chars) and ? (single char)
|
|
695
|
+
*/
|
|
696
|
+
private matchPattern(objectName: string, pattern: string): boolean {
|
|
697
|
+
const regexPattern = pattern
|
|
698
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
699
|
+
.replace(/\*/g, '.*') // * → .*
|
|
700
|
+
.replace(/\?/g, '.'); // ? → .
|
|
701
|
+
|
|
702
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
703
|
+
return regex.test(objectName);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Set datasource mapping rules
|
|
708
|
+
* Called by ObjectQLPlugin during bootstrap
|
|
709
|
+
*/
|
|
710
|
+
setDatasourceMapping(rules: Array<{
|
|
711
|
+
namespace?: string;
|
|
712
|
+
package?: string;
|
|
713
|
+
objectPattern?: string;
|
|
714
|
+
default?: boolean;
|
|
715
|
+
datasource: string;
|
|
716
|
+
priority?: number;
|
|
717
|
+
}>) {
|
|
718
|
+
this.datasourceMapping = rules;
|
|
719
|
+
this.logger.info('Datasource mapping rules configured', {
|
|
720
|
+
ruleCount: rules.length
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
604
724
|
/**
|
|
605
725
|
* Initialize the engine and all registered drivers
|
|
606
726
|
*/
|
package/src/protocol.ts
CHANGED
|
@@ -253,7 +253,14 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
253
253
|
const services = this.getServicesRegistry?.();
|
|
254
254
|
const metadataService = services?.get('metadata');
|
|
255
255
|
if (metadataService && typeof metadataService.list === 'function') {
|
|
256
|
-
|
|
256
|
+
let runtimeItems = await metadataService.list(request.type);
|
|
257
|
+
// When filtering by packageId, only include runtime items that
|
|
258
|
+
// belong to the requested package. MetadataService.list() returns
|
|
259
|
+
// items from ALL packages, so we must filter here to respect the
|
|
260
|
+
// package scope requested by the caller (e.g., Studio sidebar).
|
|
261
|
+
if (packageId && runtimeItems && runtimeItems.length > 0) {
|
|
262
|
+
runtimeItems = runtimeItems.filter((item: any) => item?._packageId === packageId);
|
|
263
|
+
}
|
|
257
264
|
if (runtimeItems && runtimeItems.length > 0) {
|
|
258
265
|
// Merge, avoiding duplicates by name
|
|
259
266
|
const itemMap = new Map<string, any>();
|