@objectstack/objectql 4.0.2 → 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 +23 -0
- package/dist/index.d.mts +72 -6
- package/dist/index.d.ts +72 -6
- package/dist/index.js +385 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +385 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/datasource-mapping.test.ts +181 -0
- package/src/engine.test.ts +15 -1
- package/src/engine.ts +253 -27
- package/src/plugin.integration.test.ts +170 -0
- package/src/plugin.ts +149 -1
- package/src/protocol.ts +107 -19
- package/src/registry.test.ts +11 -11
- package/src/registry.ts +4 -4
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,13 +13,13 @@
|
|
|
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",
|
|
22
|
-
"vitest": "^4.1.
|
|
22
|
+
"vitest": "^4.1.4"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup --config ../../tsup.config.ts",
|
|
@@ -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
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import { QueryAST, HookContext, ServiceObject } from '@objectstack/spec/data';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
5
|
EngineQueryOptions,
|
|
6
|
-
DataEngineInsertOptions,
|
|
7
|
-
EngineUpdateOptions,
|
|
6
|
+
DataEngineInsertOptions,
|
|
7
|
+
EngineUpdateOptions,
|
|
8
8
|
EngineDeleteOptions,
|
|
9
9
|
EngineAggregateOptions,
|
|
10
|
-
EngineCountOptions
|
|
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';
|
|
14
14
|
import { CoreServiceName } from '@objectstack/spec/system';
|
|
15
|
+
import { IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';
|
|
16
|
+
import { pluralToSingular } from '@objectstack/spec/shared';
|
|
15
17
|
import { SchemaRegistry } from './registry.js';
|
|
16
18
|
|
|
17
19
|
export type HookHandler = (context: HookContext) => Promise<void> | void;
|
|
@@ -69,7 +71,20 @@ export class ObjectQL implements IDataEngine {
|
|
|
69
71
|
private drivers = new Map<string, DriverInterface>();
|
|
70
72
|
private defaultDriver: string | null = null;
|
|
71
73
|
private logger: Logger;
|
|
72
|
-
|
|
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
|
+
|
|
73
88
|
// Per-object hooks with priority support
|
|
74
89
|
private hooks: Map<string, HookEntry[]> = new Map([
|
|
75
90
|
['beforeFind', []], ['afterFind', []],
|
|
@@ -86,10 +101,13 @@ export class ObjectQL implements IDataEngine {
|
|
|
86
101
|
|
|
87
102
|
// Action registry: key = "objectName:actionName"
|
|
88
103
|
private actions = new Map<string, { handler: (ctx: any) => Promise<any> | any; package?: string }>();
|
|
89
|
-
|
|
104
|
+
|
|
90
105
|
// Host provided context additions (e.g. Server router)
|
|
91
106
|
private hostContext: Record<string, any> = {};
|
|
92
107
|
|
|
108
|
+
// Realtime service for event publishing
|
|
109
|
+
private realtimeService?: IRealtimeService;
|
|
110
|
+
|
|
93
111
|
constructor(hostContext: Record<string, any> = {}) {
|
|
94
112
|
this.hostContext = hostContext;
|
|
95
113
|
// Use provided logger or create a new one
|
|
@@ -303,6 +321,11 @@ export class ObjectQL implements IDataEngine {
|
|
|
303
321
|
const namespace = manifest.namespace as string | undefined;
|
|
304
322
|
this.logger.debug('Registering package manifest', { id, namespace });
|
|
305
323
|
|
|
324
|
+
// Store manifest for defaultDatasource lookup
|
|
325
|
+
if (id) {
|
|
326
|
+
this.manifests.set(id, manifest);
|
|
327
|
+
}
|
|
328
|
+
|
|
306
329
|
// 1. Register the Package (manifest + lifecycle state)
|
|
307
330
|
SchemaRegistry.installPackage(manifest);
|
|
308
331
|
this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace });
|
|
@@ -391,7 +414,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
391
414
|
for (const item of items) {
|
|
392
415
|
const itemName = item.name || item.id;
|
|
393
416
|
if (itemName) {
|
|
394
|
-
SchemaRegistry.registerItem(key, item, 'name' as any, id);
|
|
417
|
+
SchemaRegistry.registerItem(pluralToSingular(key), item, 'name' as any, id);
|
|
395
418
|
}
|
|
396
419
|
}
|
|
397
420
|
}
|
|
@@ -497,7 +520,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
497
520
|
for (const item of items) {
|
|
498
521
|
const itemName = item.name || item.id;
|
|
499
522
|
if (itemName) {
|
|
500
|
-
SchemaRegistry.registerItem(key, item, 'name' as any, ownerId);
|
|
523
|
+
SchemaRegistry.registerItem(pluralToSingular(key), item, 'name' as any, ownerId);
|
|
501
524
|
}
|
|
502
525
|
}
|
|
503
526
|
}
|
|
@@ -514,8 +537,8 @@ export class ObjectQL implements IDataEngine {
|
|
|
514
537
|
}
|
|
515
538
|
|
|
516
539
|
this.drivers.set(driver.name, driver);
|
|
517
|
-
this.logger.info('Registered driver', {
|
|
518
|
-
driverName: driver.name,
|
|
540
|
+
this.logger.info('Registered driver', {
|
|
541
|
+
driverName: driver.name,
|
|
519
542
|
version: driver.version
|
|
520
543
|
});
|
|
521
544
|
|
|
@@ -525,6 +548,17 @@ export class ObjectQL implements IDataEngine {
|
|
|
525
548
|
}
|
|
526
549
|
}
|
|
527
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Set the realtime service for publishing data change events.
|
|
553
|
+
* Should be called after kernel resolves the realtime service.
|
|
554
|
+
*
|
|
555
|
+
* @param service - An IRealtimeService instance for event publishing
|
|
556
|
+
*/
|
|
557
|
+
setRealtimeService(service: IRealtimeService): void {
|
|
558
|
+
this.realtimeService = service;
|
|
559
|
+
this.logger.info('RealtimeService configured for data events');
|
|
560
|
+
}
|
|
561
|
+
|
|
528
562
|
/**
|
|
529
563
|
* Helper to get object definition
|
|
530
564
|
*/
|
|
@@ -555,36 +589,138 @@ export class ObjectQL implements IDataEngine {
|
|
|
555
589
|
|
|
556
590
|
/**
|
|
557
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
|
|
558
598
|
*/
|
|
559
599
|
private getDriver(objectName: string): DriverInterface {
|
|
560
600
|
const object = SchemaRegistry.getObject(objectName);
|
|
561
|
-
|
|
562
|
-
// 1.
|
|
563
|
-
if (object) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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)!;
|
|
575
634
|
}
|
|
576
|
-
throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
|
|
577
635
|
}
|
|
578
636
|
}
|
|
579
637
|
|
|
580
|
-
//
|
|
581
|
-
if (this.defaultDriver) {
|
|
638
|
+
// 4. Fallback to global default driver
|
|
639
|
+
if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
582
640
|
return this.drivers.get(this.defaultDriver)!;
|
|
583
641
|
}
|
|
584
642
|
|
|
585
643
|
throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
|
|
586
644
|
}
|
|
587
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
|
+
|
|
588
724
|
/**
|
|
589
725
|
* Initialize the engine and all registered drivers
|
|
590
726
|
*/
|
|
@@ -594,14 +730,24 @@ export class ObjectQL implements IDataEngine {
|
|
|
594
730
|
drivers: Array.from(this.drivers.keys())
|
|
595
731
|
});
|
|
596
732
|
|
|
733
|
+
const failedDrivers: string[] = [];
|
|
597
734
|
for (const [name, driver] of this.drivers) {
|
|
598
735
|
try {
|
|
599
736
|
await driver.connect();
|
|
600
737
|
this.logger.info('Driver connected successfully', { driverName: name });
|
|
601
738
|
} catch (e) {
|
|
739
|
+
failedDrivers.push(name);
|
|
602
740
|
this.logger.error('Failed to connect driver', e as Error, { driverName: name });
|
|
603
741
|
}
|
|
604
742
|
}
|
|
743
|
+
|
|
744
|
+
if (failedDrivers.length > 0) {
|
|
745
|
+
this.logger.warn(
|
|
746
|
+
`${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. ` +
|
|
747
|
+
`Operations may recover via lazy reconnection or fail at query time.`,
|
|
748
|
+
{ failedDrivers }
|
|
749
|
+
);
|
|
750
|
+
}
|
|
605
751
|
|
|
606
752
|
this.logger.info('ObjectQL engine initialization complete');
|
|
607
753
|
}
|
|
@@ -873,6 +1019,42 @@ export class ObjectQL implements IDataEngine {
|
|
|
873
1019
|
hookContext.result = result;
|
|
874
1020
|
await this.triggerHooks('afterInsert', hookContext);
|
|
875
1021
|
|
|
1022
|
+
// Publish data.record.created event to realtime service
|
|
1023
|
+
if (this.realtimeService) {
|
|
1024
|
+
try {
|
|
1025
|
+
if (Array.isArray(result)) {
|
|
1026
|
+
// Bulk insert - publish event for each record
|
|
1027
|
+
for (const record of result) {
|
|
1028
|
+
const event: RealtimeEventPayload = {
|
|
1029
|
+
type: 'data.record.created',
|
|
1030
|
+
object,
|
|
1031
|
+
payload: {
|
|
1032
|
+
recordId: record.id,
|
|
1033
|
+
after: record,
|
|
1034
|
+
},
|
|
1035
|
+
timestamp: new Date().toISOString(),
|
|
1036
|
+
};
|
|
1037
|
+
await this.realtimeService.publish(event);
|
|
1038
|
+
}
|
|
1039
|
+
this.logger.debug(`Published ${result.length} data.record.created events`, { object });
|
|
1040
|
+
} else {
|
|
1041
|
+
const event: RealtimeEventPayload = {
|
|
1042
|
+
type: 'data.record.created',
|
|
1043
|
+
object,
|
|
1044
|
+
payload: {
|
|
1045
|
+
recordId: result.id,
|
|
1046
|
+
after: result,
|
|
1047
|
+
},
|
|
1048
|
+
timestamp: new Date().toISOString(),
|
|
1049
|
+
};
|
|
1050
|
+
await this.realtimeService.publish(event);
|
|
1051
|
+
this.logger.debug('Published data.record.created event', { object, recordId: result.id });
|
|
1052
|
+
}
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
this.logger.warn('Failed to publish data event', { object, error });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
876
1058
|
return hookContext.result;
|
|
877
1059
|
} catch (e) {
|
|
878
1060
|
this.logger.error('Insert operation failed', e as Error, { object });
|
|
@@ -927,6 +1109,29 @@ export class ObjectQL implements IDataEngine {
|
|
|
927
1109
|
hookContext.event = 'afterUpdate';
|
|
928
1110
|
hookContext.result = result;
|
|
929
1111
|
await this.triggerHooks('afterUpdate', hookContext);
|
|
1112
|
+
|
|
1113
|
+
// Publish data.record.updated event to realtime service
|
|
1114
|
+
if (this.realtimeService) {
|
|
1115
|
+
try {
|
|
1116
|
+
const resultId = (typeof result === 'object' && result && 'id' in result) ? (result as any).id : undefined;
|
|
1117
|
+
const recordId = String(hookContext.input.id || resultId || '');
|
|
1118
|
+
const event: RealtimeEventPayload = {
|
|
1119
|
+
type: 'data.record.updated',
|
|
1120
|
+
object,
|
|
1121
|
+
payload: {
|
|
1122
|
+
recordId,
|
|
1123
|
+
changes: hookContext.input.data,
|
|
1124
|
+
after: result,
|
|
1125
|
+
},
|
|
1126
|
+
timestamp: new Date().toISOString(),
|
|
1127
|
+
};
|
|
1128
|
+
await this.realtimeService.publish(event);
|
|
1129
|
+
this.logger.debug('Published data.record.updated event', { object, recordId });
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
this.logger.warn('Failed to publish data event', { object, error });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
930
1135
|
return hookContext.result;
|
|
931
1136
|
} catch (e) {
|
|
932
1137
|
this.logger.error('Update operation failed', e as Error, { object });
|
|
@@ -980,6 +1185,27 @@ export class ObjectQL implements IDataEngine {
|
|
|
980
1185
|
hookContext.event = 'afterDelete';
|
|
981
1186
|
hookContext.result = result;
|
|
982
1187
|
await this.triggerHooks('afterDelete', hookContext);
|
|
1188
|
+
|
|
1189
|
+
// Publish data.record.deleted event to realtime service
|
|
1190
|
+
if (this.realtimeService) {
|
|
1191
|
+
try {
|
|
1192
|
+
const resultId = (typeof result === 'object' && result && 'id' in result) ? (result as any).id : undefined;
|
|
1193
|
+
const recordId = String(hookContext.input.id || resultId || '');
|
|
1194
|
+
const event: RealtimeEventPayload = {
|
|
1195
|
+
type: 'data.record.deleted',
|
|
1196
|
+
object,
|
|
1197
|
+
payload: {
|
|
1198
|
+
recordId,
|
|
1199
|
+
},
|
|
1200
|
+
timestamp: new Date().toISOString(),
|
|
1201
|
+
};
|
|
1202
|
+
await this.realtimeService.publish(event);
|
|
1203
|
+
this.logger.debug('Published data.record.deleted event', { object, recordId });
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
this.logger.warn('Failed to publish data event', { object, error });
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
983
1209
|
return hookContext.result;
|
|
984
1210
|
} catch (e) {
|
|
985
1211
|
this.logger.error('Delete operation failed', e as Error, { object });
|