@objectstack/objectql 4.0.4 → 4.0.5
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/dist/index.d.mts +468 -1113
- package/dist/index.d.ts +468 -1113
- package/dist/index.js +1271 -268
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1266 -268
- package/dist/index.mjs.map +1 -1
- package/package.json +32 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -720
- package/src/datasource-mapping.test.ts +0 -181
- package/src/engine.test.ts +0 -613
- package/src/engine.ts +0 -1668
- package/src/index.ts +0 -41
- package/src/kernel-factory.ts +0 -48
- package/src/metadata-facade.ts +0 -96
- package/src/plugin.integration.test.ts +0 -995
- package/src/plugin.ts +0 -534
- package/src/protocol-data.test.ts +0 -245
- package/src/protocol-discovery.test.ts +0 -213
- package/src/protocol-feed.test.ts +0 -303
- package/src/protocol-meta.test.ts +0 -440
- package/src/protocol.ts +0 -1242
- package/src/registry.test.ts +0 -494
- package/src/registry.ts +0 -716
- package/src/util.test.ts +0 -226
- package/src/util.ts +0 -219
- package/tsconfig.json +0 -10
package/src/plugin.ts
DELETED
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { ObjectQL } from './engine.js';
|
|
4
|
-
import { ObjectStackProtocolImplementation } from './protocol.js';
|
|
5
|
-
import { Plugin, PluginContext } from '@objectstack/core';
|
|
6
|
-
|
|
7
|
-
export type { Plugin, PluginContext };
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Protocol extension for DB-based metadata hydration.
|
|
11
|
-
* `loadMetaFromDb` is implemented by ObjectStackProtocolImplementation but
|
|
12
|
-
* is NOT (yet) part of the canonical ObjectStackProtocol wire-contract in
|
|
13
|
-
* `@objectstack/spec`, since it is a server-side bootstrap concern only.
|
|
14
|
-
*/
|
|
15
|
-
interface ProtocolWithDbRestore {
|
|
16
|
-
loadMetaFromDb(): Promise<{ loaded: number; errors: number }>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Type guard — checks whether the service exposes `loadMetaFromDb`. */
|
|
20
|
-
function hasLoadMetaFromDb(service: unknown): service is ProtocolWithDbRestore {
|
|
21
|
-
return (
|
|
22
|
-
typeof service === 'object' &&
|
|
23
|
-
service !== null &&
|
|
24
|
-
typeof (service as Record<string, unknown>)['loadMetaFromDb'] === 'function'
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class ObjectQLPlugin implements Plugin {
|
|
29
|
-
name = 'com.objectstack.engine.objectql';
|
|
30
|
-
type = 'objectql';
|
|
31
|
-
version = '1.0.0';
|
|
32
|
-
|
|
33
|
-
private ql: ObjectQL | undefined;
|
|
34
|
-
private hostContext?: Record<string, any>;
|
|
35
|
-
|
|
36
|
-
constructor(ql?: ObjectQL, hostContext?: Record<string, any>) {
|
|
37
|
-
if (ql) {
|
|
38
|
-
this.ql = ql;
|
|
39
|
-
} else {
|
|
40
|
-
this.hostContext = hostContext;
|
|
41
|
-
// Lazily created in init
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
init = async (ctx: PluginContext) => {
|
|
46
|
-
if (!this.ql) {
|
|
47
|
-
// Pass kernel logger to engine to avoid creating a separate pino instance
|
|
48
|
-
const hostCtx = { ...this.hostContext, logger: ctx.logger };
|
|
49
|
-
this.ql = new ObjectQL(hostCtx);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Register as provider for Core Kernel Services
|
|
53
|
-
ctx.registerService('objectql', this.ql);
|
|
54
|
-
|
|
55
|
-
ctx.registerService('data', this.ql); // ObjectQL implements IDataEngine
|
|
56
|
-
|
|
57
|
-
// Register manifest service for direct app/package registration.
|
|
58
|
-
// Plugins call ctx.getService('manifest').register(manifestData)
|
|
59
|
-
// instead of the legacy ctx.registerService('app.<id>', manifestData) convention.
|
|
60
|
-
const ql = this.ql;
|
|
61
|
-
ctx.registerService('manifest', {
|
|
62
|
-
register: (manifest: any) => {
|
|
63
|
-
ql.registerApp(manifest);
|
|
64
|
-
ctx.logger.debug('Manifest registered via manifest service', {
|
|
65
|
-
id: manifest.id || manifest.name
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
ctx.logger.info('ObjectQL engine registered', {
|
|
71
|
-
services: ['objectql', 'data', 'manifest'],
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Register Protocol Implementation
|
|
75
|
-
const protocolShim = new ObjectStackProtocolImplementation(
|
|
76
|
-
this.ql,
|
|
77
|
-
() => ctx.getServices ? ctx.getServices() : new Map()
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
ctx.registerService('protocol', protocolShim);
|
|
81
|
-
ctx.logger.info('Protocol service registered');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
start = async (ctx: PluginContext) => {
|
|
85
|
-
ctx.logger.info('ObjectQL engine starting...');
|
|
86
|
-
|
|
87
|
-
// Sync from external metadata service (e.g. MetadataPlugin) if available
|
|
88
|
-
try {
|
|
89
|
-
const metadataService = ctx.getService('metadata') as any;
|
|
90
|
-
if (metadataService && typeof metadataService.loadMany === 'function' && this.ql) {
|
|
91
|
-
await this.loadMetadataFromService(metadataService, ctx);
|
|
92
|
-
}
|
|
93
|
-
} catch (e: any) {
|
|
94
|
-
ctx.logger.debug('No external metadata service to sync from');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Discover features from Kernel Services
|
|
98
|
-
if (ctx.getServices && this.ql) {
|
|
99
|
-
const services = ctx.getServices();
|
|
100
|
-
for (const [name, service] of services.entries()) {
|
|
101
|
-
if (name.startsWith('driver.')) {
|
|
102
|
-
// Register Driver
|
|
103
|
-
this.ql.registerDriver(service);
|
|
104
|
-
ctx.logger.debug('Discovered and registered driver service', { serviceName: name });
|
|
105
|
-
}
|
|
106
|
-
if (name.startsWith('app.')) {
|
|
107
|
-
// Legacy fallback: discover app.* services (DEPRECATED)
|
|
108
|
-
ctx.logger.warn(
|
|
109
|
-
`[DEPRECATED] Service "${name}" uses legacy app.* convention. ` +
|
|
110
|
-
`Migrate to ctx.getService('manifest').register(data).`
|
|
111
|
-
);
|
|
112
|
-
this.ql.registerApp(service); // service is Manifest
|
|
113
|
-
ctx.logger.debug('Discovered and registered app service (legacy)', { serviceName: name });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Bridge realtime service from kernel service registry to ObjectQL.
|
|
118
|
-
// RealtimeServicePlugin registers as 'realtime' service during init().
|
|
119
|
-
// This enables ObjectQL to publish data change events.
|
|
120
|
-
try {
|
|
121
|
-
const realtimeService = ctx.getService('realtime');
|
|
122
|
-
if (realtimeService && typeof realtimeService === 'object' && 'publish' in realtimeService) {
|
|
123
|
-
ctx.logger.info('[ObjectQLPlugin] Bridging realtime service to ObjectQL for event publishing');
|
|
124
|
-
this.ql.setRealtimeService(realtimeService as any);
|
|
125
|
-
}
|
|
126
|
-
} catch (e: any) {
|
|
127
|
-
ctx.logger.debug('[ObjectQLPlugin] No realtime service found — data events will not be published', {
|
|
128
|
-
error: e.message,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Initialize drivers (calls driver.connect() which sets up persistence)
|
|
134
|
-
await this.ql?.init();
|
|
135
|
-
|
|
136
|
-
// Restore persisted metadata from sys_metadata table.
|
|
137
|
-
// This hydrates SchemaRegistry with objects/views/apps that were saved
|
|
138
|
-
// via protocol.saveMetaItem() in a previous session, ensuring custom
|
|
139
|
-
// schemas survive cold starts and redeployments.
|
|
140
|
-
await this.restoreMetadataFromDb(ctx);
|
|
141
|
-
|
|
142
|
-
// Sync all registered object schemas to database
|
|
143
|
-
// This ensures tables/collections are created or updated for every
|
|
144
|
-
// object registered by plugins (e.g., sys_user from plugin-auth).
|
|
145
|
-
await this.syncRegisteredSchemas(ctx);
|
|
146
|
-
|
|
147
|
-
// Bridge all SchemaRegistry objects to metadata service
|
|
148
|
-
// This ensures AI tools and other IMetadataService consumers can see all objects
|
|
149
|
-
await this.bridgeObjectsToMetadataService(ctx);
|
|
150
|
-
|
|
151
|
-
// Register built-in audit hooks
|
|
152
|
-
this.registerAuditHooks(ctx);
|
|
153
|
-
|
|
154
|
-
// Register tenant isolation middleware
|
|
155
|
-
this.registerTenantMiddleware(ctx);
|
|
156
|
-
|
|
157
|
-
ctx.logger.info('ObjectQL engine started', {
|
|
158
|
-
driversRegistered: this.ql?.['drivers']?.size || 0,
|
|
159
|
-
objectsRegistered: this.ql?.registry?.getAllObjects?.()?.length || 0
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Register built-in audit hooks for auto-stamping created_by/updated_by
|
|
165
|
-
* and fetching previousData for update/delete operations.
|
|
166
|
-
*/
|
|
167
|
-
private registerAuditHooks(ctx: PluginContext) {
|
|
168
|
-
if (!this.ql) return;
|
|
169
|
-
|
|
170
|
-
// Auto-stamp created_by/updated_by on insert
|
|
171
|
-
this.ql.registerHook('beforeInsert', async (hookCtx) => {
|
|
172
|
-
if (hookCtx.session?.userId && hookCtx.input?.data) {
|
|
173
|
-
const data = hookCtx.input.data as Record<string, any>;
|
|
174
|
-
if (typeof data === 'object' && data !== null) {
|
|
175
|
-
data.created_by = data.created_by ?? hookCtx.session.userId;
|
|
176
|
-
data.updated_by = hookCtx.session.userId;
|
|
177
|
-
data.created_at = data.created_at ?? new Date().toISOString();
|
|
178
|
-
data.updated_at = new Date().toISOString();
|
|
179
|
-
if (hookCtx.session.tenantId) {
|
|
180
|
-
data.tenant_id = data.tenant_id ?? hookCtx.session.tenantId;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}, { object: '*', priority: 10 });
|
|
185
|
-
|
|
186
|
-
// Auto-stamp updated_by on update
|
|
187
|
-
this.ql.registerHook('beforeUpdate', async (hookCtx) => {
|
|
188
|
-
if (hookCtx.session?.userId && hookCtx.input?.data) {
|
|
189
|
-
const data = hookCtx.input.data as Record<string, any>;
|
|
190
|
-
if (typeof data === 'object' && data !== null) {
|
|
191
|
-
data.updated_by = hookCtx.session.userId;
|
|
192
|
-
data.updated_at = new Date().toISOString();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}, { object: '*', priority: 10 });
|
|
196
|
-
|
|
197
|
-
// Auto-fetch previousData for update hooks
|
|
198
|
-
this.ql.registerHook('beforeUpdate', async (hookCtx) => {
|
|
199
|
-
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
200
|
-
try {
|
|
201
|
-
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
202
|
-
where: { id: hookCtx.input.id }
|
|
203
|
-
});
|
|
204
|
-
if (existing) {
|
|
205
|
-
hookCtx.previous = existing;
|
|
206
|
-
}
|
|
207
|
-
} catch (_e) {
|
|
208
|
-
// Non-fatal: some objects may not support findOne
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}, { object: '*', priority: 5 });
|
|
212
|
-
|
|
213
|
-
// Auto-fetch previousData for delete hooks
|
|
214
|
-
this.ql.registerHook('beforeDelete', async (hookCtx) => {
|
|
215
|
-
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
216
|
-
try {
|
|
217
|
-
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
218
|
-
where: { id: hookCtx.input.id }
|
|
219
|
-
});
|
|
220
|
-
if (existing) {
|
|
221
|
-
hookCtx.previous = existing;
|
|
222
|
-
}
|
|
223
|
-
} catch (_e) {
|
|
224
|
-
// Non-fatal
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}, { object: '*', priority: 5 });
|
|
228
|
-
|
|
229
|
-
ctx.logger.debug('Audit hooks registered (created_by/updated_by, previousData)');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Register tenant isolation middleware that auto-injects tenant_id filter
|
|
234
|
-
* for multi-tenant operations.
|
|
235
|
-
*/
|
|
236
|
-
private registerTenantMiddleware(ctx: PluginContext) {
|
|
237
|
-
if (!this.ql) return;
|
|
238
|
-
|
|
239
|
-
this.ql.registerMiddleware(async (opCtx, next) => {
|
|
240
|
-
// Only apply to operations with tenantId that are not system-level
|
|
241
|
-
if (!opCtx.context?.tenantId || opCtx.context?.isSystem) {
|
|
242
|
-
return next();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Read operations: inject tenant_id filter into AST
|
|
246
|
-
if (['find', 'findOne', 'count', 'aggregate'].includes(opCtx.operation)) {
|
|
247
|
-
if (opCtx.ast) {
|
|
248
|
-
const tenantFilter = { tenant_id: opCtx.context.tenantId };
|
|
249
|
-
if (opCtx.ast.where) {
|
|
250
|
-
opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] };
|
|
251
|
-
} else {
|
|
252
|
-
opCtx.ast.where = tenantFilter;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
await next();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
ctx.logger.debug('Tenant isolation middleware registered');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Synchronize all registered object schemas to the database.
|
|
265
|
-
*
|
|
266
|
-
* Groups objects by their responsible driver, then:
|
|
267
|
-
* - If the driver advertises `supports.batchSchemaSync` and implements
|
|
268
|
-
* `syncSchemasBatch()`, submits all schemas in a single call (reducing
|
|
269
|
-
* network round-trips for remote drivers like Turso).
|
|
270
|
-
* - Otherwise falls back to sequential `syncSchema()` per object.
|
|
271
|
-
*
|
|
272
|
-
* This is idempotent — drivers must tolerate repeated calls without
|
|
273
|
-
* duplicating tables or erroring out.
|
|
274
|
-
*
|
|
275
|
-
* Drivers that do not implement `syncSchema` are silently skipped.
|
|
276
|
-
*/
|
|
277
|
-
private async syncRegisteredSchemas(ctx: PluginContext) {
|
|
278
|
-
if (!this.ql) return;
|
|
279
|
-
|
|
280
|
-
const allObjects = this.ql.registry?.getAllObjects?.() ?? [];
|
|
281
|
-
if (allObjects.length === 0) return;
|
|
282
|
-
|
|
283
|
-
let synced = 0;
|
|
284
|
-
let skipped = 0;
|
|
285
|
-
|
|
286
|
-
// Group objects by driver for potential batch optimization
|
|
287
|
-
const driverGroups = new Map<any, Array<{ obj: any; tableName: string }>>();
|
|
288
|
-
|
|
289
|
-
for (const obj of allObjects) {
|
|
290
|
-
const driver = this.ql.getDriverForObject(obj.name);
|
|
291
|
-
if (!driver) {
|
|
292
|
-
ctx.logger.debug('No driver available for object, skipping schema sync', {
|
|
293
|
-
object: obj.name,
|
|
294
|
-
});
|
|
295
|
-
skipped++;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (typeof driver.syncSchema !== 'function') {
|
|
300
|
-
ctx.logger.debug('Driver does not support syncSchema, skipping', {
|
|
301
|
-
object: obj.name,
|
|
302
|
-
driver: driver.name,
|
|
303
|
-
});
|
|
304
|
-
skipped++;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const tableName = obj.tableName || obj.name;
|
|
309
|
-
|
|
310
|
-
let group = driverGroups.get(driver);
|
|
311
|
-
if (!group) {
|
|
312
|
-
group = [];
|
|
313
|
-
driverGroups.set(driver, group);
|
|
314
|
-
}
|
|
315
|
-
group.push({ obj, tableName });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Process each driver group
|
|
319
|
-
for (const [driver, entries] of driverGroups) {
|
|
320
|
-
// Batch path: driver supports batch schema sync
|
|
321
|
-
if (
|
|
322
|
-
driver.supports?.batchSchemaSync &&
|
|
323
|
-
typeof driver.syncSchemasBatch === 'function'
|
|
324
|
-
) {
|
|
325
|
-
const batchPayload = entries.map((e) => ({
|
|
326
|
-
object: e.tableName,
|
|
327
|
-
schema: e.obj,
|
|
328
|
-
}));
|
|
329
|
-
try {
|
|
330
|
-
await driver.syncSchemasBatch(batchPayload);
|
|
331
|
-
synced += entries.length;
|
|
332
|
-
ctx.logger.debug('Batch schema sync succeeded', {
|
|
333
|
-
driver: driver.name,
|
|
334
|
-
count: entries.length,
|
|
335
|
-
});
|
|
336
|
-
} catch (e: unknown) {
|
|
337
|
-
ctx.logger.warn('Batch schema sync failed, falling back to sequential', {
|
|
338
|
-
driver: driver.name,
|
|
339
|
-
error: e instanceof Error ? e.message : String(e),
|
|
340
|
-
});
|
|
341
|
-
// Fallback: sequential sync for this driver's objects
|
|
342
|
-
for (const { obj, tableName } of entries) {
|
|
343
|
-
try {
|
|
344
|
-
await driver.syncSchema(tableName, obj);
|
|
345
|
-
synced++;
|
|
346
|
-
} catch (seqErr: unknown) {
|
|
347
|
-
ctx.logger.warn('Failed to sync schema for object', {
|
|
348
|
-
object: obj.name,
|
|
349
|
-
tableName,
|
|
350
|
-
driver: driver.name,
|
|
351
|
-
error: seqErr instanceof Error ? seqErr.message : String(seqErr),
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} else {
|
|
357
|
-
// Sequential path: no batch support
|
|
358
|
-
for (const { obj, tableName } of entries) {
|
|
359
|
-
try {
|
|
360
|
-
await driver.syncSchema(tableName, obj);
|
|
361
|
-
synced++;
|
|
362
|
-
} catch (e: unknown) {
|
|
363
|
-
ctx.logger.warn('Failed to sync schema for object', {
|
|
364
|
-
object: obj.name,
|
|
365
|
-
tableName,
|
|
366
|
-
driver: driver.name,
|
|
367
|
-
error: e instanceof Error ? e.message : String(e),
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (synced > 0 || skipped > 0) {
|
|
375
|
-
ctx.logger.info('Schema sync complete', { synced, skipped, total: allObjects.length });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Restore persisted metadata from the database (sys_metadata) on startup.
|
|
381
|
-
*
|
|
382
|
-
* Calls `protocol.loadMetaFromDb()` to bulk-load all active metadata
|
|
383
|
-
* records (objects, views, apps, etc.) into the in-memory SchemaRegistry.
|
|
384
|
-
* This closes the persistence loop so that user-created schemas survive
|
|
385
|
-
* kernel cold starts and redeployments.
|
|
386
|
-
*
|
|
387
|
-
* Gracefully degrades when:
|
|
388
|
-
* - The protocol service is unavailable (e.g., in-memory-only mode).
|
|
389
|
-
* - `loadMetaFromDb` is not implemented by the protocol shim.
|
|
390
|
-
* - The underlying driver/table does not exist yet (first-run scenario).
|
|
391
|
-
*/
|
|
392
|
-
private async restoreMetadataFromDb(ctx: PluginContext): Promise<void> {
|
|
393
|
-
// Phase 1: Resolve protocol service (separate from DB I/O for clearer diagnostics)
|
|
394
|
-
let protocol: ProtocolWithDbRestore;
|
|
395
|
-
try {
|
|
396
|
-
const service = ctx.getService('protocol');
|
|
397
|
-
if (!service || !hasLoadMetaFromDb(service)) {
|
|
398
|
-
ctx.logger.debug('Protocol service does not support loadMetaFromDb, skipping DB restore');
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
protocol = service;
|
|
402
|
-
} catch (e: unknown) {
|
|
403
|
-
ctx.logger.debug('Protocol service unavailable, skipping DB restore', {
|
|
404
|
-
error: e instanceof Error ? e.message : String(e),
|
|
405
|
-
});
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Phase 2: DB hydration (loads into SchemaRegistry)
|
|
410
|
-
try {
|
|
411
|
-
const { loaded, errors } = await protocol.loadMetaFromDb();
|
|
412
|
-
|
|
413
|
-
if (loaded > 0 || errors > 0) {
|
|
414
|
-
ctx.logger.info('Metadata restored from database to SchemaRegistry', { loaded, errors });
|
|
415
|
-
} else {
|
|
416
|
-
ctx.logger.debug('No persisted metadata found in database');
|
|
417
|
-
}
|
|
418
|
-
} catch (e: unknown) {
|
|
419
|
-
// Non-fatal: first-run or in-memory driver may not have sys_metadata yet
|
|
420
|
-
ctx.logger.debug('DB metadata restore failed (non-fatal)', {
|
|
421
|
-
error: e instanceof Error ? e.message : String(e),
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Bridge all SchemaRegistry objects to the metadata service.
|
|
428
|
-
*
|
|
429
|
-
* This ensures objects registered by plugins and loaded from sys_metadata
|
|
430
|
-
* are visible to AI tools and other consumers that query IMetadataService.
|
|
431
|
-
*
|
|
432
|
-
* Runs after both restoreMetadataFromDb() and syncRegisteredSchemas() to
|
|
433
|
-
* catch all objects in the SchemaRegistry regardless of their source.
|
|
434
|
-
*/
|
|
435
|
-
private async bridgeObjectsToMetadataService(ctx: PluginContext): Promise<void> {
|
|
436
|
-
try {
|
|
437
|
-
const metadataService = ctx.getService<any>('metadata');
|
|
438
|
-
if (!metadataService || typeof metadataService.register !== 'function') {
|
|
439
|
-
ctx.logger.debug('Metadata service unavailable for bridging, skipping');
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (!this.ql?.registry) {
|
|
444
|
-
ctx.logger.debug('SchemaRegistry unavailable for bridging, skipping');
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const objects = this.ql.registry.getAllObjects();
|
|
449
|
-
let bridged = 0;
|
|
450
|
-
|
|
451
|
-
for (const obj of objects) {
|
|
452
|
-
try {
|
|
453
|
-
// Check if object is already in metadata service to avoid duplicates
|
|
454
|
-
const existing = await metadataService.getObject(obj.name);
|
|
455
|
-
if (!existing) {
|
|
456
|
-
// Register object that exists in SchemaRegistry but not in metadata service
|
|
457
|
-
await metadataService.register('object', obj.name, obj);
|
|
458
|
-
bridged++;
|
|
459
|
-
}
|
|
460
|
-
} catch (e: unknown) {
|
|
461
|
-
ctx.logger.debug('Failed to bridge object to metadata service', {
|
|
462
|
-
object: obj.name,
|
|
463
|
-
error: e instanceof Error ? e.message : String(e),
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (bridged > 0) {
|
|
469
|
-
ctx.logger.info('Bridged objects from SchemaRegistry to metadata service', {
|
|
470
|
-
count: bridged,
|
|
471
|
-
total: objects.length
|
|
472
|
-
});
|
|
473
|
-
} else {
|
|
474
|
-
ctx.logger.debug('No objects needed bridging (all already in metadata service)');
|
|
475
|
-
}
|
|
476
|
-
} catch (e: unknown) {
|
|
477
|
-
ctx.logger.debug('Failed to bridge objects to metadata service', {
|
|
478
|
-
error: e instanceof Error ? e.message : String(e),
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Load metadata from external metadata service into ObjectQL registry
|
|
485
|
-
* This enables ObjectQL to use file-based or remote metadata
|
|
486
|
-
*/
|
|
487
|
-
private async loadMetadataFromService(metadataService: any, ctx: PluginContext) {
|
|
488
|
-
ctx.logger.info('Syncing metadata from external service into ObjectQL registry...');
|
|
489
|
-
|
|
490
|
-
// Metadata types to sync
|
|
491
|
-
const metadataTypes = ['object', 'view', 'app', 'flow', 'workflow', 'function'];
|
|
492
|
-
let totalLoaded = 0;
|
|
493
|
-
|
|
494
|
-
for (const type of metadataTypes) {
|
|
495
|
-
try {
|
|
496
|
-
// Check if service has loadMany method
|
|
497
|
-
if (typeof metadataService.loadMany === 'function') {
|
|
498
|
-
const items = await metadataService.loadMany(type);
|
|
499
|
-
|
|
500
|
-
if (items && items.length > 0) {
|
|
501
|
-
items.forEach((item: any) => {
|
|
502
|
-
// Determine key field (usually 'name' or 'id')
|
|
503
|
-
const keyField = item.id ? 'id' : 'name';
|
|
504
|
-
|
|
505
|
-
// For objects, use the ownership-aware registration
|
|
506
|
-
if (type === 'object' && this.ql) {
|
|
507
|
-
// Objects are registered differently (ownership model)
|
|
508
|
-
// Skip for now - handled by app registration
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Register other types in the registry
|
|
513
|
-
if (this.ql?.registry?.registerItem) {
|
|
514
|
-
this.ql.registry.registerItem(type, item, keyField);
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
totalLoaded += items.length;
|
|
519
|
-
ctx.logger.info(`Synced ${items.length} ${type}(s) from metadata service`);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} catch (e: any) {
|
|
523
|
-
// Type might not exist in metadata service - that's ok
|
|
524
|
-
ctx.logger.debug(`No ${type} metadata found or error loading`, {
|
|
525
|
-
error: e.message
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (totalLoaded > 0) {
|
|
531
|
-
ctx.logger.info(`Metadata sync complete: ${totalLoaded} items loaded into ObjectQL registry`);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|