@objectstack/objectql 4.0.1 → 4.0.3
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 +11 -11
- package/CHANGELOG.md +23 -0
- package/dist/index.d.mts +60 -68
- package/dist/index.d.ts +60 -68
- package/dist/index.js +336 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +336 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/engine.ts +116 -10
- package/src/plugin.integration.test.ts +245 -13
- package/src/plugin.ts +174 -46
- package/src/protocol.ts +110 -25
- package/src/registry.test.ts +27 -16
- package/src/registry.ts +34 -25
package/src/plugin.ts
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import { ObjectQL } from './engine.js';
|
|
4
|
-
import { MetadataFacade } from './metadata-facade.js';
|
|
5
4
|
import { ObjectStackProtocolImplementation } from './protocol.js';
|
|
6
5
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
7
6
|
|
|
8
7
|
export type { Plugin, PluginContext };
|
|
9
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
|
+
|
|
10
28
|
export class ObjectQLPlugin implements Plugin {
|
|
11
29
|
name = 'com.objectstack.engine.objectql';
|
|
12
30
|
type = 'objectql';
|
|
@@ -33,45 +51,24 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
33
51
|
|
|
34
52
|
// Register as provider for Core Kernel Services
|
|
35
53
|
ctx.registerService('objectql', this.ql);
|
|
36
|
-
|
|
37
|
-
// Register MetadataFacade as metadata service (unless external service exists)
|
|
38
|
-
let hasMetadata = false;
|
|
39
|
-
let metadataProvider = 'objectql';
|
|
40
|
-
try {
|
|
41
|
-
if (ctx.getService('metadata')) {
|
|
42
|
-
hasMetadata = true;
|
|
43
|
-
metadataProvider = 'external';
|
|
44
|
-
}
|
|
45
|
-
} catch (e: any) {
|
|
46
|
-
// Ignore errors during check (e.g. "Service is async")
|
|
47
|
-
}
|
|
48
54
|
|
|
49
|
-
if (!hasMetadata) {
|
|
50
|
-
try {
|
|
51
|
-
const metadataFacade = new MetadataFacade();
|
|
52
|
-
ctx.registerService('metadata', metadataFacade);
|
|
53
|
-
ctx.logger.info('MetadataFacade registered as metadata service', {
|
|
54
|
-
mode: 'in-memory',
|
|
55
|
-
features: ['registry', 'fast-lookup']
|
|
56
|
-
});
|
|
57
|
-
} catch (e: any) {
|
|
58
|
-
// Ignore if already registered (race condition or async mis-detection)
|
|
59
|
-
if (!e.message?.includes('already registered')) {
|
|
60
|
-
throw e;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
ctx.logger.info('External metadata service detected', {
|
|
65
|
-
provider: metadataProvider,
|
|
66
|
-
mode: 'will-sync-in-start-phase'
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
55
|
ctx.registerService('data', this.ql); // ObjectQL implements IDataEngine
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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'],
|
|
75
72
|
});
|
|
76
73
|
|
|
77
74
|
// Register Protocol Implementation
|
|
@@ -86,16 +83,14 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
86
83
|
|
|
87
84
|
start = async (ctx: PluginContext) => {
|
|
88
85
|
ctx.logger.info('ObjectQL engine starting...');
|
|
89
|
-
|
|
90
|
-
//
|
|
86
|
+
|
|
87
|
+
// Sync from external metadata service (e.g. MetadataPlugin) if available
|
|
91
88
|
try {
|
|
92
89
|
const metadataService = ctx.getService('metadata') as any;
|
|
93
|
-
|
|
94
|
-
if (metadataService && !(metadataService instanceof MetadataFacade) && this.ql) {
|
|
90
|
+
if (metadataService && typeof metadataService.loadMany === 'function' && this.ql) {
|
|
95
91
|
await this.loadMetadataFromService(metadataService, ctx);
|
|
96
92
|
}
|
|
97
93
|
} catch (e: any) {
|
|
98
|
-
// No external metadata service or error accessing it
|
|
99
94
|
ctx.logger.debug('No external metadata service to sync from');
|
|
100
95
|
}
|
|
101
96
|
|
|
@@ -109,27 +104,56 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
109
104
|
ctx.logger.debug('Discovered and registered driver service', { serviceName: name });
|
|
110
105
|
}
|
|
111
106
|
if (name.startsWith('app.')) {
|
|
112
|
-
//
|
|
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
|
+
);
|
|
113
112
|
this.ql.registerApp(service); // service is Manifest
|
|
114
|
-
ctx.logger.debug('Discovered and registered app service', { serviceName: name });
|
|
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);
|
|
115
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
|
+
});
|
|
116
130
|
}
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
// Initialize drivers (calls driver.connect() which sets up persistence)
|
|
120
134
|
await this.ql?.init();
|
|
121
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
|
+
|
|
122
142
|
// Sync all registered object schemas to database
|
|
123
143
|
// This ensures tables/collections are created or updated for every
|
|
124
144
|
// object registered by plugins (e.g., sys_user from plugin-auth).
|
|
125
145
|
await this.syncRegisteredSchemas(ctx);
|
|
126
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
|
+
|
|
127
151
|
// Register built-in audit hooks
|
|
128
152
|
this.registerAuditHooks(ctx);
|
|
129
153
|
|
|
130
154
|
// Register tenant isolation middleware
|
|
131
155
|
this.registerTenantMiddleware(ctx);
|
|
132
|
-
|
|
156
|
+
|
|
133
157
|
ctx.logger.info('ObjectQL engine started', {
|
|
134
158
|
driversRegistered: this.ql?.['drivers']?.size || 0,
|
|
135
159
|
objectsRegistered: this.ql?.registry?.getAllObjects?.()?.length || 0
|
|
@@ -352,6 +376,110 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
352
376
|
}
|
|
353
377
|
}
|
|
354
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
|
+
|
|
355
483
|
/**
|
|
356
484
|
* Load metadata from external metadata service into ObjectQL registry
|
|
357
485
|
* This enables ObjectQL to use file-based or remote metadata
|
package/src/protocol.ts
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { ObjectStackProtocol } from '@objectstack/spec/api';
|
|
4
4
|
import { IDataEngine } from '@objectstack/core';
|
|
5
|
-
import type {
|
|
6
|
-
BatchUpdateRequest,
|
|
7
|
-
BatchUpdateResponse,
|
|
5
|
+
import type {
|
|
6
|
+
BatchUpdateRequest,
|
|
7
|
+
BatchUpdateResponse,
|
|
8
8
|
UpdateManyDataRequest,
|
|
9
9
|
DeleteManyDataRequest
|
|
10
10
|
} from '@objectstack/spec/api';
|
|
11
11
|
import type { MetadataCacheRequest, MetadataCacheResponse, ServiceInfo, ApiRoutes, WellKnownCapabilities } from '@objectstack/spec/api';
|
|
12
12
|
import type { IFeedService } from '@objectstack/spec/contracts';
|
|
13
13
|
import { parseFilterAST, isFilterAST } from '@objectstack/spec/data';
|
|
14
|
+
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
|
|
14
15
|
|
|
15
16
|
// We import SchemaRegistry directly since this class lives in the same package
|
|
16
17
|
import { SchemaRegistry } from './registry.js';
|
|
@@ -180,24 +181,40 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
async getMetaTypes() {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
const schemaTypes = SchemaRegistry.getRegisteredTypes();
|
|
185
|
+
|
|
186
|
+
// Also include types from MetadataService (runtime-registered: agent, tool, etc.)
|
|
187
|
+
let runtimeTypes: string[] = [];
|
|
188
|
+
try {
|
|
189
|
+
const services = this.getServicesRegistry?.();
|
|
190
|
+
const metadataService = services?.get('metadata');
|
|
191
|
+
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
|
|
192
|
+
runtimeTypes = await metadataService.getRegisteredTypes();
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// MetadataService not available
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const allTypes = Array.from(new Set([...schemaTypes, ...runtimeTypes]));
|
|
199
|
+
return { types: allTypes };
|
|
186
200
|
}
|
|
187
201
|
|
|
188
|
-
async getMetaItems(request: { type: string }) {
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
async getMetaItems(request: { type: string; packageId?: string }) {
|
|
203
|
+
const { packageId } = request;
|
|
204
|
+
let items = SchemaRegistry.listItems(request.type, packageId);
|
|
205
|
+
// Normalize singular/plural using explicit mapping
|
|
191
206
|
if (items.length === 0) {
|
|
192
|
-
const alt = request.type
|
|
193
|
-
items = SchemaRegistry.listItems(alt);
|
|
207
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
208
|
+
if (alt) items = SchemaRegistry.listItems(alt, packageId);
|
|
194
209
|
}
|
|
195
210
|
|
|
196
211
|
// Fallback to database if registry is empty for this type
|
|
197
212
|
if (items.length === 0) {
|
|
198
213
|
try {
|
|
214
|
+
const whereClause: any = { type: request.type, state: 'active' };
|
|
215
|
+
if (packageId) whereClause._packageId = packageId;
|
|
199
216
|
const allRecords = await this.engine.find('sys_metadata', {
|
|
200
|
-
where:
|
|
217
|
+
where: whereClause
|
|
201
218
|
});
|
|
202
219
|
if (allRecords && allRecords.length > 0) {
|
|
203
220
|
items = allRecords.map((record: any) => {
|
|
@@ -209,8 +226,9 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
209
226
|
return data;
|
|
210
227
|
});
|
|
211
228
|
} else {
|
|
212
|
-
// Try alternate type name in DB
|
|
213
|
-
const alt = request.type
|
|
229
|
+
// Try alternate type name in DB using explicit mapping
|
|
230
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
231
|
+
if (alt) {
|
|
214
232
|
const altRecords = await this.engine.find('sys_metadata', {
|
|
215
233
|
where: { type: alt, state: 'active' }
|
|
216
234
|
});
|
|
@@ -223,24 +241,53 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
223
241
|
return data;
|
|
224
242
|
});
|
|
225
243
|
}
|
|
244
|
+
}
|
|
226
245
|
}
|
|
227
246
|
} catch {
|
|
228
247
|
// DB not available, return registry results (empty)
|
|
229
248
|
}
|
|
230
249
|
}
|
|
231
250
|
|
|
251
|
+
// Merge with MetadataService (runtime-registered items: agents, tools, etc.)
|
|
252
|
+
try {
|
|
253
|
+
const services = this.getServicesRegistry?.();
|
|
254
|
+
const metadataService = services?.get('metadata');
|
|
255
|
+
if (metadataService && typeof metadataService.list === 'function') {
|
|
256
|
+
const runtimeItems = await metadataService.list(request.type);
|
|
257
|
+
if (runtimeItems && runtimeItems.length > 0) {
|
|
258
|
+
// Merge, avoiding duplicates by name
|
|
259
|
+
const itemMap = new Map<string, any>();
|
|
260
|
+
for (const item of items) {
|
|
261
|
+
const entry = item as any;
|
|
262
|
+
if (entry && typeof entry === 'object' && 'name' in entry) {
|
|
263
|
+
itemMap.set(entry.name, entry);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const item of runtimeItems) {
|
|
267
|
+
const entry = item as any;
|
|
268
|
+
if (entry && typeof entry === 'object' && 'name' in entry) {
|
|
269
|
+
itemMap.set(entry.name, entry);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
items = Array.from(itemMap.values());
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// MetadataService not available or doesn't support this type
|
|
277
|
+
}
|
|
278
|
+
|
|
232
279
|
return {
|
|
233
280
|
type: request.type,
|
|
234
281
|
items
|
|
235
282
|
};
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
async getMetaItem(request: { type: string, name: string }) {
|
|
285
|
+
async getMetaItem(request: { type: string, name: string, packageId?: string }) {
|
|
239
286
|
let item = SchemaRegistry.getItem(request.type, request.name);
|
|
240
|
-
// Normalize singular/plural
|
|
287
|
+
// Normalize singular/plural using explicit mapping
|
|
241
288
|
if (item === undefined) {
|
|
242
|
-
const alt = request.type
|
|
243
|
-
item = SchemaRegistry.getItem(alt, request.name);
|
|
289
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
290
|
+
if (alt) item = SchemaRegistry.getItem(alt, request.name);
|
|
244
291
|
}
|
|
245
292
|
|
|
246
293
|
// Fallback to database if not in registry
|
|
@@ -256,8 +303,9 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
256
303
|
// Hydrate back into registry for next time
|
|
257
304
|
SchemaRegistry.registerItem(request.type, item, 'name' as any);
|
|
258
305
|
} else {
|
|
259
|
-
// Try alternate type name
|
|
260
|
-
const alt = request.type
|
|
306
|
+
// Try alternate type name using explicit mapping
|
|
307
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
308
|
+
if (alt) {
|
|
261
309
|
const altRecord = await this.engine.findOne('sys_metadata', {
|
|
262
310
|
where: { type: alt, name: request.name, state: 'active' }
|
|
263
311
|
});
|
|
@@ -268,12 +316,26 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
268
316
|
// Hydrate back into registry for next time
|
|
269
317
|
SchemaRegistry.registerItem(request.type, item, 'name' as any);
|
|
270
318
|
}
|
|
319
|
+
}
|
|
271
320
|
}
|
|
272
321
|
} catch {
|
|
273
322
|
// DB not available, return undefined
|
|
274
323
|
}
|
|
275
324
|
}
|
|
276
325
|
|
|
326
|
+
// Fallback to MetadataService for runtime-registered items (agents, tools, etc.)
|
|
327
|
+
if (item === undefined) {
|
|
328
|
+
try {
|
|
329
|
+
const services = this.getServicesRegistry?.();
|
|
330
|
+
const metadataService = services?.get('metadata');
|
|
331
|
+
if (metadataService && typeof metadataService.get === 'function') {
|
|
332
|
+
item = await metadataService.get(request.type, request.name);
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
// MetadataService not available
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
277
339
|
return {
|
|
278
340
|
type: request.type,
|
|
279
341
|
name: request.name,
|
|
@@ -481,10 +543,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
481
543
|
}
|
|
482
544
|
|
|
483
545
|
const records = await this.engine.find(request.object, options);
|
|
546
|
+
// Spec: FindDataResponseSchema — only `records` is returned.
|
|
547
|
+
// OData `value` adaptation (if needed) is handled in the HTTP dispatch layer.
|
|
484
548
|
return {
|
|
485
549
|
object: request.object,
|
|
486
|
-
|
|
487
|
-
records, // Legacy
|
|
550
|
+
records,
|
|
488
551
|
total: records.length,
|
|
489
552
|
hasMore: false
|
|
490
553
|
};
|
|
@@ -559,7 +622,27 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
559
622
|
|
|
560
623
|
async getMetaItemCached(request: { type: string, name: string, cacheRequest?: MetadataCacheRequest }): Promise<MetadataCacheResponse> {
|
|
561
624
|
try {
|
|
562
|
-
|
|
625
|
+
let item = SchemaRegistry.getItem(request.type, request.name);
|
|
626
|
+
|
|
627
|
+
// Normalize singular/plural using explicit mapping
|
|
628
|
+
if (!item) {
|
|
629
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
630
|
+
if (alt) item = SchemaRegistry.getItem(alt, request.name);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Fallback to MetadataService (e.g. agents, tools registered in MetadataManager)
|
|
634
|
+
if (!item) {
|
|
635
|
+
try {
|
|
636
|
+
const services = this.getServicesRegistry?.();
|
|
637
|
+
const metadataService = services?.get('metadata');
|
|
638
|
+
if (metadataService && typeof metadataService.get === 'function') {
|
|
639
|
+
item = await metadataService.get(request.type, request.name);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// MetadataService not available
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
563
646
|
if (!item) {
|
|
564
647
|
throw new Error(`Metadata item ${request.type}/${request.name} not found`);
|
|
565
648
|
}
|
|
@@ -980,10 +1063,12 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
980
1063
|
const data = typeof record.metadata === 'string'
|
|
981
1064
|
? JSON.parse(record.metadata)
|
|
982
1065
|
: record.metadata;
|
|
983
|
-
|
|
1066
|
+
// Normalize DB type to singular (DB may store legacy plural forms)
|
|
1067
|
+
const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
|
|
1068
|
+
if (normalizedType === 'object') {
|
|
984
1069
|
SchemaRegistry.registerObject(data as any, record.packageId || 'sys_metadata');
|
|
985
1070
|
} else {
|
|
986
|
-
SchemaRegistry.registerItem(
|
|
1071
|
+
SchemaRegistry.registerItem(normalizedType, data, 'name' as any);
|
|
987
1072
|
}
|
|
988
1073
|
loaded++;
|
|
989
1074
|
} catch (e) {
|
package/src/registry.test.ts
CHANGED
|
@@ -53,11 +53,15 @@ describe('SchemaRegistry', () => {
|
|
|
53
53
|
}).not.toThrow();
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it('should
|
|
57
|
-
SchemaRegistry.registerNamespace('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
it('should allow multiple packages to share a namespace', () => {
|
|
57
|
+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.auth');
|
|
58
|
+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.security');
|
|
59
|
+
// First registered package returned for backwards compat
|
|
60
|
+
expect(SchemaRegistry.getNamespaceOwner('sys')).toBe('com.objectstack.auth');
|
|
61
|
+
expect(SchemaRegistry.getNamespaceOwners('sys')).toEqual([
|
|
62
|
+
'com.objectstack.auth',
|
|
63
|
+
'com.objectstack.security',
|
|
64
|
+
]);
|
|
61
65
|
});
|
|
62
66
|
|
|
63
67
|
it('should unregister namespace', () => {
|
|
@@ -65,6 +69,13 @@ describe('SchemaRegistry', () => {
|
|
|
65
69
|
SchemaRegistry.unregisterNamespace('crm', 'com.example.crm');
|
|
66
70
|
expect(SchemaRegistry.getNamespaceOwner('crm')).toBeUndefined();
|
|
67
71
|
});
|
|
72
|
+
|
|
73
|
+
it('should keep namespace when one of multiple packages unregisters', () => {
|
|
74
|
+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.auth');
|
|
75
|
+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.setup');
|
|
76
|
+
SchemaRegistry.unregisterNamespace('sys', 'com.objectstack.setup');
|
|
77
|
+
expect(SchemaRegistry.getNamespaceOwner('sys')).toBe('com.objectstack.auth');
|
|
78
|
+
});
|
|
68
79
|
});
|
|
69
80
|
|
|
70
81
|
// ==========================================
|
|
@@ -342,17 +353,17 @@ describe('SchemaRegistry', () => {
|
|
|
342
353
|
describe('Generic Metadata', () => {
|
|
343
354
|
it('should register and retrieve generic items', () => {
|
|
344
355
|
const item = { name: 'test_action', type: 'custom' };
|
|
345
|
-
SchemaRegistry.registerItem('
|
|
346
|
-
|
|
347
|
-
const retrieved = SchemaRegistry.getItem('
|
|
356
|
+
SchemaRegistry.registerItem('action', item, 'name', 'com.pkg');
|
|
357
|
+
|
|
358
|
+
const retrieved = SchemaRegistry.getItem('action', 'test_action');
|
|
348
359
|
expect(retrieved).toEqual(item);
|
|
349
360
|
});
|
|
350
361
|
|
|
351
362
|
it('should list items by type with package filter', () => {
|
|
352
|
-
SchemaRegistry.registerItem('
|
|
353
|
-
SchemaRegistry.registerItem('
|
|
354
|
-
|
|
355
|
-
const filtered = SchemaRegistry.listItems('
|
|
363
|
+
SchemaRegistry.registerItem('action', { name: 'a1' }, 'name', 'com.pkg1');
|
|
364
|
+
SchemaRegistry.registerItem('action', { name: 'a2' }, 'name', 'com.pkg2');
|
|
365
|
+
|
|
366
|
+
const filtered = SchemaRegistry.listItems('action', 'com.pkg1');
|
|
356
367
|
expect(filtered).toHaveLength(1);
|
|
357
368
|
});
|
|
358
369
|
});
|
|
@@ -385,12 +396,12 @@ describe('SchemaRegistry', () => {
|
|
|
385
396
|
describe('Reset', () => {
|
|
386
397
|
it('should clear all state', () => {
|
|
387
398
|
SchemaRegistry.registerObject({ name: 'obj', fields: {} } as any, 'com.pkg', 'pkg', 'own');
|
|
388
|
-
SchemaRegistry.registerItem('
|
|
389
|
-
|
|
399
|
+
SchemaRegistry.registerItem('action', { name: 'act' }, 'name');
|
|
400
|
+
|
|
390
401
|
SchemaRegistry.reset();
|
|
391
|
-
|
|
402
|
+
|
|
392
403
|
expect(SchemaRegistry.getAllObjects()).toHaveLength(0);
|
|
393
|
-
expect(SchemaRegistry.listItems('
|
|
404
|
+
expect(SchemaRegistry.listItems('action')).toHaveLength(0);
|
|
394
405
|
});
|
|
395
406
|
});
|
|
396
407
|
|