@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.
@@ -822,4 +822,174 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
822
822
  expect(singleCalls).toContain('nb__item');
823
823
  });
824
824
  });
825
+
826
+ describe('Cold-Start Metadata Restoration', () => {
827
+ it('should restore metadata from sys_metadata via protocol.loadMetaFromDb on start', async () => {
828
+ // Arrange — a driver whose find() returns persisted metadata records
829
+ const findCalls: Array<{ object: string; query: any }> = [];
830
+ const mockDriver = {
831
+ name: 'restore-driver',
832
+ version: '1.0.0',
833
+ connect: async () => {},
834
+ disconnect: async () => {},
835
+ find: async (object: string, query: any) => {
836
+ findCalls.push({ object, query });
837
+ if (object === 'sys_metadata') {
838
+ return [
839
+ {
840
+ id: '1',
841
+ type: 'apps',
842
+ name: 'custom_crm',
843
+ state: 'active',
844
+ metadata: JSON.stringify({ name: 'custom_crm', label: 'Custom CRM' }),
845
+ },
846
+ {
847
+ id: '2',
848
+ type: 'object',
849
+ name: 'invoice',
850
+ state: 'active',
851
+ metadata: JSON.stringify({
852
+ name: 'invoice',
853
+ label: 'Invoice',
854
+ fields: { amount: { name: 'amount', label: 'Amount', type: 'number' } },
855
+ }),
856
+ packageId: 'user_pkg',
857
+ },
858
+ ];
859
+ }
860
+ return [];
861
+ },
862
+ findOne: async () => null,
863
+ create: async (_o: string, d: any) => d,
864
+ update: async (_o: string, _i: any, d: any) => d,
865
+ delete: async () => true,
866
+ syncSchema: async () => {},
867
+ };
868
+
869
+ await kernel.use({
870
+ name: 'mock-restore-driver',
871
+ type: 'driver',
872
+ version: '1.0.0',
873
+ init: async (ctx) => {
874
+ ctx.registerService('driver.restore', mockDriver);
875
+ },
876
+ });
877
+
878
+ const plugin = new ObjectQLPlugin();
879
+ await kernel.use(plugin);
880
+
881
+ // Act
882
+ await kernel.bootstrap();
883
+
884
+ // Assert — sys_metadata should have been queried
885
+ const metaQuery = findCalls.find((c) => c.object === 'sys_metadata');
886
+ expect(metaQuery).toBeDefined();
887
+ expect(metaQuery!.query.where).toEqual({ state: 'active' });
888
+
889
+ // Assert — items should be restored into the registry
890
+ const registry = (kernel.getService('objectql') as any).registry;
891
+ expect(registry.getAllApps()).toContainEqual({
892
+ name: 'custom_crm',
893
+ label: 'Custom CRM',
894
+ });
895
+ });
896
+
897
+ it('should not throw when protocol.loadMetaFromDb fails (graceful degradation)', async () => {
898
+ // Arrange — driver that throws on find('sys_metadata')
899
+ const mockDriver = {
900
+ name: 'failing-db-driver',
901
+ version: '1.0.0',
902
+ connect: async () => {},
903
+ disconnect: async () => {},
904
+ find: async (object: string) => {
905
+ if (object === 'sys_metadata') {
906
+ throw new Error('SQLITE_ERROR: no such table: sys_metadata');
907
+ }
908
+ return [];
909
+ },
910
+ findOne: async () => null,
911
+ create: async (_o: string, d: any) => d,
912
+ update: async (_o: string, _i: any, d: any) => d,
913
+ delete: async () => true,
914
+ syncSchema: async () => {},
915
+ };
916
+
917
+ await kernel.use({
918
+ name: 'mock-fail-driver',
919
+ type: 'driver',
920
+ version: '1.0.0',
921
+ init: async (ctx) => {
922
+ ctx.registerService('driver.faildb', mockDriver);
923
+ },
924
+ });
925
+
926
+ const plugin = new ObjectQLPlugin();
927
+ await kernel.use(plugin);
928
+
929
+ // Act & Assert — should not throw
930
+ await expect(kernel.bootstrap()).resolves.not.toThrow();
931
+ });
932
+
933
+ it('should restore metadata before syncRegisteredSchemas so restored objects get table sync', async () => {
934
+ // Arrange — track the order of operations
935
+ const operations: string[] = [];
936
+ const mockDriver = {
937
+ name: 'order-driver',
938
+ version: '1.0.0',
939
+ connect: async () => {},
940
+ disconnect: async () => {},
941
+ find: async (object: string) => {
942
+ if (object === 'sys_metadata') {
943
+ operations.push('loadMetaFromDb');
944
+ return [
945
+ {
946
+ id: '1',
947
+ type: 'object',
948
+ name: 'restored_obj',
949
+ state: 'active',
950
+ metadata: JSON.stringify({
951
+ name: 'restored_obj',
952
+ label: 'Restored Object',
953
+ fields: { title: { name: 'title', label: 'Title', type: 'text' } },
954
+ }),
955
+ packageId: 'user_pkg',
956
+ },
957
+ ];
958
+ }
959
+ return [];
960
+ },
961
+ findOne: async () => null,
962
+ create: async (_o: string, d: any) => d,
963
+ update: async (_o: string, _i: any, d: any) => d,
964
+ delete: async () => true,
965
+ syncSchema: async (object: string) => {
966
+ operations.push(`syncSchema:${object}`);
967
+ },
968
+ };
969
+
970
+ await kernel.use({
971
+ name: 'mock-order-driver',
972
+ type: 'driver',
973
+ version: '1.0.0',
974
+ init: async (ctx) => {
975
+ ctx.registerService('driver.order', mockDriver);
976
+ },
977
+ });
978
+
979
+ const plugin = new ObjectQLPlugin();
980
+ await kernel.use(plugin);
981
+
982
+ // Act
983
+ await kernel.bootstrap();
984
+
985
+ // Assert — loadMetaFromDb must appear before any syncSchema call
986
+ const loadIdx = operations.indexOf('loadMetaFromDb');
987
+ expect(loadIdx).toBeGreaterThanOrEqual(0);
988
+
989
+ const firstSync = operations.findIndex((op) => op.startsWith('syncSchema:'));
990
+ if (firstSync >= 0) {
991
+ expect(loadIdx).toBeLessThan(firstSync);
992
+ }
993
+ });
994
+ });
825
995
  });
package/src/plugin.ts CHANGED
@@ -6,6 +6,25 @@ import { Plugin, PluginContext } from '@objectstack/core';
6
6
 
7
7
  export type { Plugin, PluginContext };
8
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
+
9
28
  export class ObjectQLPlugin implements Plugin {
10
29
  name = 'com.objectstack.engine.objectql';
11
30
  type = 'objectql';
@@ -94,22 +113,47 @@ export class ObjectQLPlugin implements Plugin {
94
113
  ctx.logger.debug('Discovered and registered app service (legacy)', { serviceName: name });
95
114
  }
96
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
+ }
97
131
  }
98
132
 
99
133
  // Initialize drivers (calls driver.connect() which sets up persistence)
100
134
  await this.ql?.init();
101
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
+
102
142
  // Sync all registered object schemas to database
103
143
  // This ensures tables/collections are created or updated for every
104
144
  // object registered by plugins (e.g., sys_user from plugin-auth).
105
145
  await this.syncRegisteredSchemas(ctx);
106
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
+
107
151
  // Register built-in audit hooks
108
152
  this.registerAuditHooks(ctx);
109
153
 
110
154
  // Register tenant isolation middleware
111
155
  this.registerTenantMiddleware(ctx);
112
-
156
+
113
157
  ctx.logger.info('ObjectQL engine started', {
114
158
  driversRegistered: this.ql?.['drivers']?.size || 0,
115
159
  objectsRegistered: this.ql?.registry?.getAllObjects?.()?.length || 0
@@ -332,6 +376,110 @@ export class ObjectQLPlugin implements Plugin {
332
376
  }
333
377
  }
334
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
+
335
483
  /**
336
484
  * Load metadata from external metadata service into ObjectQL registry
337
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,18 +181,31 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
180
181
  }
181
182
 
182
183
  async getMetaTypes() {
183
- return {
184
- types: SchemaRegistry.getRegisteredTypes()
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
202
  async getMetaItems(request: { type: string; packageId?: string }) {
189
203
  const { packageId } = request;
190
204
  let items = SchemaRegistry.listItems(request.type, packageId);
191
- // Normalize singular/plural: REST uses singular ('app') but registry may store as plural ('apps')
205
+ // Normalize singular/plural using explicit mapping
192
206
  if (items.length === 0) {
193
- const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
194
- items = SchemaRegistry.listItems(alt, packageId);
207
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
208
+ if (alt) items = SchemaRegistry.listItems(alt, packageId);
195
209
  }
196
210
 
197
211
  // Fallback to database if registry is empty for this type
@@ -212,8 +226,9 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
212
226
  return data;
213
227
  });
214
228
  } else {
215
- // Try alternate type name in DB
216
- const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
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) {
217
232
  const altRecords = await this.engine.find('sys_metadata', {
218
233
  where: { type: alt, state: 'active' }
219
234
  });
@@ -226,12 +241,48 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
226
241
  return data;
227
242
  });
228
243
  }
244
+ }
229
245
  }
230
246
  } catch {
231
247
  // DB not available, return registry results (empty)
232
248
  }
233
249
  }
234
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
+ 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
+ }
264
+ if (runtimeItems && runtimeItems.length > 0) {
265
+ // Merge, avoiding duplicates by name
266
+ const itemMap = new Map<string, any>();
267
+ for (const item of items) {
268
+ const entry = item as any;
269
+ if (entry && typeof entry === 'object' && 'name' in entry) {
270
+ itemMap.set(entry.name, entry);
271
+ }
272
+ }
273
+ for (const item of runtimeItems) {
274
+ const entry = item as any;
275
+ if (entry && typeof entry === 'object' && 'name' in entry) {
276
+ itemMap.set(entry.name, entry);
277
+ }
278
+ }
279
+ items = Array.from(itemMap.values());
280
+ }
281
+ }
282
+ } catch {
283
+ // MetadataService not available or doesn't support this type
284
+ }
285
+
235
286
  return {
236
287
  type: request.type,
237
288
  items
@@ -240,10 +291,10 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
240
291
 
241
292
  async getMetaItem(request: { type: string, name: string, packageId?: string }) {
242
293
  let item = SchemaRegistry.getItem(request.type, request.name);
243
- // Normalize singular/plural
294
+ // Normalize singular/plural using explicit mapping
244
295
  if (item === undefined) {
245
- const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
246
- item = SchemaRegistry.getItem(alt, request.name);
296
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
297
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
247
298
  }
248
299
 
249
300
  // Fallback to database if not in registry
@@ -259,8 +310,9 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
259
310
  // Hydrate back into registry for next time
260
311
  SchemaRegistry.registerItem(request.type, item, 'name' as any);
261
312
  } else {
262
- // Try alternate type name
263
- const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
313
+ // Try alternate type name using explicit mapping
314
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
315
+ if (alt) {
264
316
  const altRecord = await this.engine.findOne('sys_metadata', {
265
317
  where: { type: alt, name: request.name, state: 'active' }
266
318
  });
@@ -271,12 +323,26 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
271
323
  // Hydrate back into registry for next time
272
324
  SchemaRegistry.registerItem(request.type, item, 'name' as any);
273
325
  }
326
+ }
274
327
  }
275
328
  } catch {
276
329
  // DB not available, return undefined
277
330
  }
278
331
  }
279
332
 
333
+ // Fallback to MetadataService for runtime-registered items (agents, tools, etc.)
334
+ if (item === undefined) {
335
+ try {
336
+ const services = this.getServicesRegistry?.();
337
+ const metadataService = services?.get('metadata');
338
+ if (metadataService && typeof metadataService.get === 'function') {
339
+ item = await metadataService.get(request.type, request.name);
340
+ }
341
+ } catch {
342
+ // MetadataService not available
343
+ }
344
+ }
345
+
280
346
  return {
281
347
  type: request.type,
282
348
  name: request.name,
@@ -563,7 +629,27 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
563
629
 
564
630
  async getMetaItemCached(request: { type: string, name: string, cacheRequest?: MetadataCacheRequest }): Promise<MetadataCacheResponse> {
565
631
  try {
566
- const item = SchemaRegistry.getItem(request.type, request.name);
632
+ let item = SchemaRegistry.getItem(request.type, request.name);
633
+
634
+ // Normalize singular/plural using explicit mapping
635
+ if (!item) {
636
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
637
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
638
+ }
639
+
640
+ // Fallback to MetadataService (e.g. agents, tools registered in MetadataManager)
641
+ if (!item) {
642
+ try {
643
+ const services = this.getServicesRegistry?.();
644
+ const metadataService = services?.get('metadata');
645
+ if (metadataService && typeof metadataService.get === 'function') {
646
+ item = await metadataService.get(request.type, request.name);
647
+ }
648
+ } catch {
649
+ // MetadataService not available
650
+ }
651
+ }
652
+
567
653
  if (!item) {
568
654
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
569
655
  }
@@ -984,10 +1070,12 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
984
1070
  const data = typeof record.metadata === 'string'
985
1071
  ? JSON.parse(record.metadata)
986
1072
  : record.metadata;
987
- if (record.type === 'object') {
1073
+ // Normalize DB type to singular (DB may store legacy plural forms)
1074
+ const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
1075
+ if (normalizedType === 'object') {
988
1076
  SchemaRegistry.registerObject(data as any, record.packageId || 'sys_metadata');
989
1077
  } else {
990
- SchemaRegistry.registerItem(record.type, data, 'name' as any);
1078
+ SchemaRegistry.registerItem(normalizedType, data, 'name' as any);
991
1079
  }
992
1080
  loaded++;
993
1081
  } catch (e) {
@@ -353,17 +353,17 @@ describe('SchemaRegistry', () => {
353
353
  describe('Generic Metadata', () => {
354
354
  it('should register and retrieve generic items', () => {
355
355
  const item = { name: 'test_action', type: 'custom' };
356
- SchemaRegistry.registerItem('actions', item, 'name', 'com.pkg');
357
-
358
- const retrieved = SchemaRegistry.getItem('actions', 'test_action');
356
+ SchemaRegistry.registerItem('action', item, 'name', 'com.pkg');
357
+
358
+ const retrieved = SchemaRegistry.getItem('action', 'test_action');
359
359
  expect(retrieved).toEqual(item);
360
360
  });
361
361
 
362
362
  it('should list items by type with package filter', () => {
363
- SchemaRegistry.registerItem('actions', { name: 'a1' }, 'name', 'com.pkg1');
364
- SchemaRegistry.registerItem('actions', { name: 'a2' }, 'name', 'com.pkg2');
365
-
366
- const filtered = SchemaRegistry.listItems('actions', 'com.pkg1');
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');
367
367
  expect(filtered).toHaveLength(1);
368
368
  });
369
369
  });
@@ -396,12 +396,12 @@ describe('SchemaRegistry', () => {
396
396
  describe('Reset', () => {
397
397
  it('should clear all state', () => {
398
398
  SchemaRegistry.registerObject({ name: 'obj', fields: {} } as any, 'com.pkg', 'pkg', 'own');
399
- SchemaRegistry.registerItem('actions', { name: 'act' }, 'name');
400
-
399
+ SchemaRegistry.registerItem('action', { name: 'act' }, 'name');
400
+
401
401
  SchemaRegistry.reset();
402
-
402
+
403
403
  expect(SchemaRegistry.getAllObjects()).toHaveLength(0);
404
- expect(SchemaRegistry.listItems('actions')).toHaveLength(0);
404
+ expect(SchemaRegistry.listItems('action')).toHaveLength(0);
405
405
  });
406
406
  });
407
407
 
package/src/registry.ts CHANGED
@@ -485,7 +485,7 @@ export class SchemaRegistry {
485
485
  if (type === 'object') {
486
486
  return ObjectSchema.parse(item);
487
487
  }
488
- if (type === 'apps') {
488
+ if (type === 'app') {
489
489
  return AppSchema.parse(item);
490
490
  }
491
491
  if (type === 'package') {
@@ -664,15 +664,15 @@ export class SchemaRegistry {
664
664
  // ==========================================
665
665
 
666
666
  static registerApp(app: any, packageId?: string) {
667
- this.registerItem('apps', app, 'name', packageId);
667
+ this.registerItem('app', app, 'name', packageId);
668
668
  }
669
669
 
670
670
  static getApp(name: string): any {
671
- return this.getItem('apps', name);
671
+ return this.getItem('app', name);
672
672
  }
673
673
 
674
674
  static getAllApps(): any[] {
675
- return this.listItems('apps');
675
+ return this.listItems('app');
676
676
  }
677
677
 
678
678
  // ==========================================