@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/dist/index.mjs CHANGED
@@ -53,36 +53,45 @@ var SchemaRegistry = class {
53
53
  // ==========================================
54
54
  /**
55
55
  * Register a namespace for a package.
56
- * Enforces namespace uniqueness within the instance.
57
- *
58
- * @throws Error if namespace is already registered to a different package
56
+ * Multiple packages can share the same namespace (e.g. 'sys').
59
57
  */
60
58
  static registerNamespace(namespace, packageId) {
61
59
  if (!namespace) return;
62
- const existing = this.namespaceRegistry.get(namespace);
63
- if (existing && existing !== packageId) {
64
- throw new Error(
65
- `Namespace "${namespace}" is already registered to package "${existing}". Package "${packageId}" cannot use the same namespace.`
66
- );
60
+ let owners = this.namespaceRegistry.get(namespace);
61
+ if (!owners) {
62
+ owners = /* @__PURE__ */ new Set();
63
+ this.namespaceRegistry.set(namespace, owners);
67
64
  }
68
- this.namespaceRegistry.set(namespace, packageId);
65
+ owners.add(packageId);
69
66
  this.log(`[Registry] Registered namespace: ${namespace} \u2192 ${packageId}`);
70
67
  }
71
68
  /**
72
69
  * Unregister a namespace when a package is uninstalled.
73
70
  */
74
71
  static unregisterNamespace(namespace, packageId) {
75
- const existing = this.namespaceRegistry.get(namespace);
76
- if (existing === packageId) {
77
- this.namespaceRegistry.delete(namespace);
78
- this.log(`[Registry] Unregistered namespace: ${namespace}`);
72
+ const owners = this.namespaceRegistry.get(namespace);
73
+ if (owners) {
74
+ owners.delete(packageId);
75
+ if (owners.size === 0) {
76
+ this.namespaceRegistry.delete(namespace);
77
+ }
78
+ this.log(`[Registry] Unregistered namespace: ${namespace} \u2190 ${packageId}`);
79
79
  }
80
80
  }
81
81
  /**
82
- * Get the package that owns a namespace.
82
+ * Get the packages that use a namespace.
83
83
  */
84
84
  static getNamespaceOwner(namespace) {
85
- return this.namespaceRegistry.get(namespace);
85
+ const owners = this.namespaceRegistry.get(namespace);
86
+ if (!owners || owners.size === 0) return void 0;
87
+ return owners.values().next().value;
88
+ }
89
+ /**
90
+ * Get all packages that share a namespace.
91
+ */
92
+ static getNamespaceOwners(namespace) {
93
+ const owners = this.namespaceRegistry.get(namespace);
94
+ return owners ? Array.from(owners) : [];
86
95
  }
87
96
  // ==========================================
88
97
  // Object Registration (Ownership Model)
@@ -292,7 +301,7 @@ var SchemaRegistry = class {
292
301
  if (type === "object") {
293
302
  return ObjectSchema.parse(item);
294
303
  }
295
- if (type === "apps") {
304
+ if (type === "app") {
296
305
  return AppSchema.parse(item);
297
306
  }
298
307
  if (type === "package") {
@@ -442,13 +451,13 @@ var SchemaRegistry = class {
442
451
  // App Helpers
443
452
  // ==========================================
444
453
  static registerApp(app, packageId) {
445
- this.registerItem("apps", app, "name", packageId);
454
+ this.registerItem("app", app, "name", packageId);
446
455
  }
447
456
  static getApp(name) {
448
- return this.getItem("apps", name);
457
+ return this.getItem("app", name);
449
458
  }
450
459
  static getAllApps() {
451
- return this.listItems("apps");
460
+ return this.listItems("app");
452
461
  }
453
462
  // ==========================================
454
463
  // Plugin Helpers
@@ -494,7 +503,7 @@ SchemaRegistry._logLevel = "info";
494
503
  SchemaRegistry.objectContributors = /* @__PURE__ */ new Map();
495
504
  /** FQN → Merged ServiceObject (cached, invalidated on changes) */
496
505
  SchemaRegistry.mergedObjectCache = /* @__PURE__ */ new Map();
497
- /** Namespace → PackageId (ensures namespace uniqueness) */
506
+ /** Namespace → Set<PackageId> (multiple packages can share a namespace) */
498
507
  SchemaRegistry.namespaceRegistry = /* @__PURE__ */ new Map();
499
508
  // ==========================================
500
509
  // Generic metadata storage (non-object types)
@@ -504,6 +513,7 @@ SchemaRegistry.metadata = /* @__PURE__ */ new Map();
504
513
 
505
514
  // src/protocol.ts
506
515
  import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
516
+ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared";
507
517
  function simpleHash(str) {
508
518
  let hash = 0;
509
519
  for (let i = 0; i < str.length; i++) {
@@ -631,20 +641,32 @@ var ObjectStackProtocolImplementation = class {
631
641
  };
632
642
  }
633
643
  async getMetaTypes() {
634
- return {
635
- types: SchemaRegistry.getRegisteredTypes()
636
- };
644
+ const schemaTypes = SchemaRegistry.getRegisteredTypes();
645
+ let runtimeTypes = [];
646
+ try {
647
+ const services = this.getServicesRegistry?.();
648
+ const metadataService = services?.get("metadata");
649
+ if (metadataService && typeof metadataService.getRegisteredTypes === "function") {
650
+ runtimeTypes = await metadataService.getRegisteredTypes();
651
+ }
652
+ } catch {
653
+ }
654
+ const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
655
+ return { types: allTypes };
637
656
  }
638
657
  async getMetaItems(request) {
639
- let items = SchemaRegistry.listItems(request.type);
658
+ const { packageId } = request;
659
+ let items = SchemaRegistry.listItems(request.type, packageId);
640
660
  if (items.length === 0) {
641
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
642
- items = SchemaRegistry.listItems(alt);
661
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
662
+ if (alt) items = SchemaRegistry.listItems(alt, packageId);
643
663
  }
644
664
  if (items.length === 0) {
645
665
  try {
666
+ const whereClause = { type: request.type, state: "active" };
667
+ if (packageId) whereClause._packageId = packageId;
646
668
  const allRecords = await this.engine.find("sys_metadata", {
647
- where: { type: request.type, state: "active" }
669
+ where: whereClause
648
670
  });
649
671
  if (allRecords && allRecords.length > 0) {
650
672
  items = allRecords.map((record) => {
@@ -653,21 +675,47 @@ var ObjectStackProtocolImplementation = class {
653
675
  return data;
654
676
  });
655
677
  } else {
656
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
657
- const altRecords = await this.engine.find("sys_metadata", {
658
- where: { type: alt, state: "active" }
659
- });
660
- if (altRecords && altRecords.length > 0) {
661
- items = altRecords.map((record) => {
662
- const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
663
- SchemaRegistry.registerItem(request.type, data, "name");
664
- return data;
678
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
679
+ if (alt) {
680
+ const altRecords = await this.engine.find("sys_metadata", {
681
+ where: { type: alt, state: "active" }
665
682
  });
683
+ if (altRecords && altRecords.length > 0) {
684
+ items = altRecords.map((record) => {
685
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
686
+ SchemaRegistry.registerItem(request.type, data, "name");
687
+ return data;
688
+ });
689
+ }
666
690
  }
667
691
  }
668
692
  } catch {
669
693
  }
670
694
  }
695
+ try {
696
+ const services = this.getServicesRegistry?.();
697
+ const metadataService = services?.get("metadata");
698
+ if (metadataService && typeof metadataService.list === "function") {
699
+ const runtimeItems = await metadataService.list(request.type);
700
+ if (runtimeItems && runtimeItems.length > 0) {
701
+ const itemMap = /* @__PURE__ */ new Map();
702
+ for (const item of items) {
703
+ const entry = item;
704
+ if (entry && typeof entry === "object" && "name" in entry) {
705
+ itemMap.set(entry.name, entry);
706
+ }
707
+ }
708
+ for (const item of runtimeItems) {
709
+ const entry = item;
710
+ if (entry && typeof entry === "object" && "name" in entry) {
711
+ itemMap.set(entry.name, entry);
712
+ }
713
+ }
714
+ items = Array.from(itemMap.values());
715
+ }
716
+ }
717
+ } catch {
718
+ }
671
719
  return {
672
720
  type: request.type,
673
721
  items
@@ -676,8 +724,8 @@ var ObjectStackProtocolImplementation = class {
676
724
  async getMetaItem(request) {
677
725
  let item = SchemaRegistry.getItem(request.type, request.name);
678
726
  if (item === void 0) {
679
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
680
- item = SchemaRegistry.getItem(alt, request.name);
727
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
728
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
681
729
  }
682
730
  if (item === void 0) {
683
731
  try {
@@ -688,18 +736,30 @@ var ObjectStackProtocolImplementation = class {
688
736
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
689
737
  SchemaRegistry.registerItem(request.type, item, "name");
690
738
  } else {
691
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
692
- const altRecord = await this.engine.findOne("sys_metadata", {
693
- where: { type: alt, name: request.name, state: "active" }
694
- });
695
- if (altRecord) {
696
- item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
697
- SchemaRegistry.registerItem(request.type, item, "name");
739
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
740
+ if (alt) {
741
+ const altRecord = await this.engine.findOne("sys_metadata", {
742
+ where: { type: alt, name: request.name, state: "active" }
743
+ });
744
+ if (altRecord) {
745
+ item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
746
+ SchemaRegistry.registerItem(request.type, item, "name");
747
+ }
698
748
  }
699
749
  }
700
750
  } catch {
701
751
  }
702
752
  }
753
+ if (item === void 0) {
754
+ try {
755
+ const services = this.getServicesRegistry?.();
756
+ const metadataService = services?.get("metadata");
757
+ if (metadataService && typeof metadataService.get === "function") {
758
+ item = await metadataService.get(request.type, request.name);
759
+ }
760
+ } catch {
761
+ }
762
+ }
703
763
  return {
704
764
  type: request.type,
705
765
  name: request.name,
@@ -872,10 +932,7 @@ var ObjectStackProtocolImplementation = class {
872
932
  const records = await this.engine.find(request.object, options);
873
933
  return {
874
934
  object: request.object,
875
- value: records,
876
- // OData compatibility
877
935
  records,
878
- // Legacy
879
936
  total: records.length,
880
937
  hasMore: false
881
938
  };
@@ -933,7 +990,21 @@ var ObjectStackProtocolImplementation = class {
933
990
  // ==========================================
934
991
  async getMetaItemCached(request) {
935
992
  try {
936
- const item = SchemaRegistry.getItem(request.type, request.name);
993
+ let item = SchemaRegistry.getItem(request.type, request.name);
994
+ if (!item) {
995
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
996
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
997
+ }
998
+ if (!item) {
999
+ try {
1000
+ const services = this.getServicesRegistry?.();
1001
+ const metadataService = services?.get("metadata");
1002
+ if (metadataService && typeof metadataService.get === "function") {
1003
+ item = await metadataService.get(request.type, request.name);
1004
+ }
1005
+ } catch {
1006
+ }
1007
+ }
937
1008
  if (!item) {
938
1009
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
939
1010
  }
@@ -1285,10 +1356,11 @@ var ObjectStackProtocolImplementation = class {
1285
1356
  for (const record of records) {
1286
1357
  try {
1287
1358
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1288
- if (record.type === "object") {
1359
+ const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
1360
+ if (normalizedType === "object") {
1289
1361
  SchemaRegistry.registerObject(data, record.packageId || "sys_metadata");
1290
1362
  } else {
1291
- SchemaRegistry.registerItem(record.type, data, "name");
1363
+ SchemaRegistry.registerItem(normalizedType, data, "name");
1292
1364
  }
1293
1365
  loaded++;
1294
1366
  } catch (e) {
@@ -1438,6 +1510,7 @@ var ObjectStackProtocolImplementation = class {
1438
1510
  import { ExecutionContextSchema } from "@objectstack/spec/kernel";
1439
1511
  import { createLogger } from "@objectstack/core";
1440
1512
  import { CoreServiceName } from "@objectstack/spec/system";
1513
+ import { pluralToSingular } from "@objectstack/spec/shared";
1441
1514
  var _ObjectQL = class _ObjectQL {
1442
1515
  constructor(hostContext = {}) {
1443
1516
  this.drivers = /* @__PURE__ */ new Map();
@@ -1728,7 +1801,7 @@ var _ObjectQL = class _ObjectQL {
1728
1801
  for (const item of items) {
1729
1802
  const itemName = item.name || item.id;
1730
1803
  if (itemName) {
1731
- SchemaRegistry.registerItem(key, item, "name", id);
1804
+ SchemaRegistry.registerItem(pluralToSingular(key), item, "name", id);
1732
1805
  }
1733
1806
  }
1734
1807
  }
@@ -1834,7 +1907,7 @@ var _ObjectQL = class _ObjectQL {
1834
1907
  for (const item of items) {
1835
1908
  const itemName = item.name || item.id;
1836
1909
  if (itemName) {
1837
- SchemaRegistry.registerItem(key, item, "name", ownerId);
1910
+ SchemaRegistry.registerItem(pluralToSingular(key), item, "name", ownerId);
1838
1911
  }
1839
1912
  }
1840
1913
  }
@@ -1858,6 +1931,16 @@ var _ObjectQL = class _ObjectQL {
1858
1931
  this.logger.info("Set default driver", { driverName: driver.name });
1859
1932
  }
1860
1933
  }
1934
+ /**
1935
+ * Set the realtime service for publishing data change events.
1936
+ * Should be called after kernel resolves the realtime service.
1937
+ *
1938
+ * @param service - An IRealtimeService instance for event publishing
1939
+ */
1940
+ setRealtimeService(service) {
1941
+ this.realtimeService = service;
1942
+ this.logger.info("RealtimeService configured for data events");
1943
+ }
1861
1944
  /**
1862
1945
  * Helper to get object definition
1863
1946
  */
@@ -1912,14 +1995,22 @@ var _ObjectQL = class _ObjectQL {
1912
1995
  driverCount: this.drivers.size,
1913
1996
  drivers: Array.from(this.drivers.keys())
1914
1997
  });
1998
+ const failedDrivers = [];
1915
1999
  for (const [name, driver] of this.drivers) {
1916
2000
  try {
1917
2001
  await driver.connect();
1918
2002
  this.logger.info("Driver connected successfully", { driverName: name });
1919
2003
  } catch (e) {
2004
+ failedDrivers.push(name);
1920
2005
  this.logger.error("Failed to connect driver", e, { driverName: name });
1921
2006
  }
1922
2007
  }
2008
+ if (failedDrivers.length > 0) {
2009
+ this.logger.warn(
2010
+ `${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. Operations may recover via lazy reconnection or fail at query time.`,
2011
+ { failedDrivers }
2012
+ );
2013
+ }
1923
2014
  this.logger.info("ObjectQL engine initialization complete");
1924
2015
  }
1925
2016
  async destroy() {
@@ -2121,6 +2212,39 @@ var _ObjectQL = class _ObjectQL {
2121
2212
  hookContext.event = "afterInsert";
2122
2213
  hookContext.result = result;
2123
2214
  await this.triggerHooks("afterInsert", hookContext);
2215
+ if (this.realtimeService) {
2216
+ try {
2217
+ if (Array.isArray(result)) {
2218
+ for (const record of result) {
2219
+ const event = {
2220
+ type: "data.record.created",
2221
+ object,
2222
+ payload: {
2223
+ recordId: record.id,
2224
+ after: record
2225
+ },
2226
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2227
+ };
2228
+ await this.realtimeService.publish(event);
2229
+ }
2230
+ this.logger.debug(`Published ${result.length} data.record.created events`, { object });
2231
+ } else {
2232
+ const event = {
2233
+ type: "data.record.created",
2234
+ object,
2235
+ payload: {
2236
+ recordId: result.id,
2237
+ after: result
2238
+ },
2239
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2240
+ };
2241
+ await this.realtimeService.publish(event);
2242
+ this.logger.debug("Published data.record.created event", { object, recordId: result.id });
2243
+ }
2244
+ } catch (error) {
2245
+ this.logger.warn("Failed to publish data event", { object, error });
2246
+ }
2247
+ }
2124
2248
  return hookContext.result;
2125
2249
  } catch (e) {
2126
2250
  this.logger.error("Insert operation failed", e, { object });
@@ -2167,6 +2291,26 @@ var _ObjectQL = class _ObjectQL {
2167
2291
  hookContext.event = "afterUpdate";
2168
2292
  hookContext.result = result;
2169
2293
  await this.triggerHooks("afterUpdate", hookContext);
2294
+ if (this.realtimeService) {
2295
+ try {
2296
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2297
+ const recordId = String(hookContext.input.id || resultId || "");
2298
+ const event = {
2299
+ type: "data.record.updated",
2300
+ object,
2301
+ payload: {
2302
+ recordId,
2303
+ changes: hookContext.input.data,
2304
+ after: result
2305
+ },
2306
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2307
+ };
2308
+ await this.realtimeService.publish(event);
2309
+ this.logger.debug("Published data.record.updated event", { object, recordId });
2310
+ } catch (error) {
2311
+ this.logger.warn("Failed to publish data event", { object, error });
2312
+ }
2313
+ }
2170
2314
  return hookContext.result;
2171
2315
  } catch (e) {
2172
2316
  this.logger.error("Update operation failed", e, { object });
@@ -2212,6 +2356,24 @@ var _ObjectQL = class _ObjectQL {
2212
2356
  hookContext.event = "afterDelete";
2213
2357
  hookContext.result = result;
2214
2358
  await this.triggerHooks("afterDelete", hookContext);
2359
+ if (this.realtimeService) {
2360
+ try {
2361
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2362
+ const recordId = String(hookContext.input.id || resultId || "");
2363
+ const event = {
2364
+ type: "data.record.deleted",
2365
+ object,
2366
+ payload: {
2367
+ recordId
2368
+ },
2369
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2370
+ };
2371
+ await this.realtimeService.publish(event);
2372
+ this.logger.debug("Published data.record.deleted event", { object, recordId });
2373
+ } catch (error) {
2374
+ this.logger.warn("Failed to publish data event", { object, error });
2375
+ }
2376
+ }
2215
2377
  return hookContext.result;
2216
2378
  } catch (e) {
2217
2379
  this.logger.error("Delete operation failed", e, { object });
@@ -2669,6 +2831,9 @@ var MetadataFacade = class {
2669
2831
  };
2670
2832
 
2671
2833
  // src/plugin.ts
2834
+ function hasLoadMetaFromDb(service) {
2835
+ return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
2836
+ }
2672
2837
  var ObjectQLPlugin = class {
2673
2838
  constructor(ql, hostContext) {
2674
2839
  this.name = "com.objectstack.engine.objectql";
@@ -2680,38 +2845,18 @@ var ObjectQLPlugin = class {
2680
2845
  this.ql = new ObjectQL(hostCtx);
2681
2846
  }
2682
2847
  ctx.registerService("objectql", this.ql);
2683
- let hasMetadata = false;
2684
- let metadataProvider = "objectql";
2685
- try {
2686
- if (ctx.getService("metadata")) {
2687
- hasMetadata = true;
2688
- metadataProvider = "external";
2689
- }
2690
- } catch (e) {
2691
- }
2692
- if (!hasMetadata) {
2693
- try {
2694
- const metadataFacade = new MetadataFacade();
2695
- ctx.registerService("metadata", metadataFacade);
2696
- ctx.logger.info("MetadataFacade registered as metadata service", {
2697
- mode: "in-memory",
2698
- features: ["registry", "fast-lookup"]
2848
+ ctx.registerService("data", this.ql);
2849
+ const ql = this.ql;
2850
+ ctx.registerService("manifest", {
2851
+ register: (manifest) => {
2852
+ ql.registerApp(manifest);
2853
+ ctx.logger.debug("Manifest registered via manifest service", {
2854
+ id: manifest.id || manifest.name
2699
2855
  });
2700
- } catch (e) {
2701
- if (!e.message?.includes("already registered")) {
2702
- throw e;
2703
- }
2704
2856
  }
2705
- } else {
2706
- ctx.logger.info("External metadata service detected", {
2707
- provider: metadataProvider,
2708
- mode: "will-sync-in-start-phase"
2709
- });
2710
- }
2711
- ctx.registerService("data", this.ql);
2857
+ });
2712
2858
  ctx.logger.info("ObjectQL engine registered", {
2713
- services: ["objectql", "data"],
2714
- metadataProvider
2859
+ services: ["objectql", "data", "manifest"]
2715
2860
  });
2716
2861
  const protocolShim = new ObjectStackProtocolImplementation(
2717
2862
  this.ql,
@@ -2724,7 +2869,7 @@ var ObjectQLPlugin = class {
2724
2869
  ctx.logger.info("ObjectQL engine starting...");
2725
2870
  try {
2726
2871
  const metadataService = ctx.getService("metadata");
2727
- if (metadataService && !(metadataService instanceof MetadataFacade) && this.ql) {
2872
+ if (metadataService && typeof metadataService.loadMany === "function" && this.ql) {
2728
2873
  await this.loadMetadataFromService(metadataService, ctx);
2729
2874
  }
2730
2875
  } catch (e) {
@@ -2738,13 +2883,29 @@ var ObjectQLPlugin = class {
2738
2883
  ctx.logger.debug("Discovered and registered driver service", { serviceName: name });
2739
2884
  }
2740
2885
  if (name.startsWith("app.")) {
2886
+ ctx.logger.warn(
2887
+ `[DEPRECATED] Service "${name}" uses legacy app.* convention. Migrate to ctx.getService('manifest').register(data).`
2888
+ );
2741
2889
  this.ql.registerApp(service);
2742
- ctx.logger.debug("Discovered and registered app service", { serviceName: name });
2890
+ ctx.logger.debug("Discovered and registered app service (legacy)", { serviceName: name });
2891
+ }
2892
+ }
2893
+ try {
2894
+ const realtimeService = ctx.getService("realtime");
2895
+ if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
2896
+ ctx.logger.info("[ObjectQLPlugin] Bridging realtime service to ObjectQL for event publishing");
2897
+ this.ql.setRealtimeService(realtimeService);
2743
2898
  }
2899
+ } catch (e) {
2900
+ ctx.logger.debug("[ObjectQLPlugin] No realtime service found \u2014 data events will not be published", {
2901
+ error: e.message
2902
+ });
2744
2903
  }
2745
2904
  }
2746
2905
  await this.ql?.init();
2906
+ await this.restoreMetadataFromDb(ctx);
2747
2907
  await this.syncRegisteredSchemas(ctx);
2908
+ await this.bridgeObjectsToMetadataService(ctx);
2748
2909
  this.registerAuditHooks(ctx);
2749
2910
  this.registerTenantMiddleware(ctx);
2750
2911
  ctx.logger.info("ObjectQL engine started", {
@@ -2937,6 +3098,97 @@ var ObjectQLPlugin = class {
2937
3098
  ctx.logger.info("Schema sync complete", { synced, skipped, total: allObjects.length });
2938
3099
  }
2939
3100
  }
3101
+ /**
3102
+ * Restore persisted metadata from the database (sys_metadata) on startup.
3103
+ *
3104
+ * Calls `protocol.loadMetaFromDb()` to bulk-load all active metadata
3105
+ * records (objects, views, apps, etc.) into the in-memory SchemaRegistry.
3106
+ * This closes the persistence loop so that user-created schemas survive
3107
+ * kernel cold starts and redeployments.
3108
+ *
3109
+ * Gracefully degrades when:
3110
+ * - The protocol service is unavailable (e.g., in-memory-only mode).
3111
+ * - `loadMetaFromDb` is not implemented by the protocol shim.
3112
+ * - The underlying driver/table does not exist yet (first-run scenario).
3113
+ */
3114
+ async restoreMetadataFromDb(ctx) {
3115
+ let protocol;
3116
+ try {
3117
+ const service = ctx.getService("protocol");
3118
+ if (!service || !hasLoadMetaFromDb(service)) {
3119
+ ctx.logger.debug("Protocol service does not support loadMetaFromDb, skipping DB restore");
3120
+ return;
3121
+ }
3122
+ protocol = service;
3123
+ } catch (e) {
3124
+ ctx.logger.debug("Protocol service unavailable, skipping DB restore", {
3125
+ error: e instanceof Error ? e.message : String(e)
3126
+ });
3127
+ return;
3128
+ }
3129
+ try {
3130
+ const { loaded, errors } = await protocol.loadMetaFromDb();
3131
+ if (loaded > 0 || errors > 0) {
3132
+ ctx.logger.info("Metadata restored from database to SchemaRegistry", { loaded, errors });
3133
+ } else {
3134
+ ctx.logger.debug("No persisted metadata found in database");
3135
+ }
3136
+ } catch (e) {
3137
+ ctx.logger.debug("DB metadata restore failed (non-fatal)", {
3138
+ error: e instanceof Error ? e.message : String(e)
3139
+ });
3140
+ }
3141
+ }
3142
+ /**
3143
+ * Bridge all SchemaRegistry objects to the metadata service.
3144
+ *
3145
+ * This ensures objects registered by plugins and loaded from sys_metadata
3146
+ * are visible to AI tools and other consumers that query IMetadataService.
3147
+ *
3148
+ * Runs after both restoreMetadataFromDb() and syncRegisteredSchemas() to
3149
+ * catch all objects in the SchemaRegistry regardless of their source.
3150
+ */
3151
+ async bridgeObjectsToMetadataService(ctx) {
3152
+ try {
3153
+ const metadataService = ctx.getService("metadata");
3154
+ if (!metadataService || typeof metadataService.register !== "function") {
3155
+ ctx.logger.debug("Metadata service unavailable for bridging, skipping");
3156
+ return;
3157
+ }
3158
+ if (!this.ql?.registry) {
3159
+ ctx.logger.debug("SchemaRegistry unavailable for bridging, skipping");
3160
+ return;
3161
+ }
3162
+ const objects = this.ql.registry.getAllObjects();
3163
+ let bridged = 0;
3164
+ for (const obj of objects) {
3165
+ try {
3166
+ const existing = await metadataService.getObject(obj.name);
3167
+ if (!existing) {
3168
+ await metadataService.register("object", obj.name, obj);
3169
+ bridged++;
3170
+ }
3171
+ } catch (e) {
3172
+ ctx.logger.debug("Failed to bridge object to metadata service", {
3173
+ object: obj.name,
3174
+ error: e instanceof Error ? e.message : String(e)
3175
+ });
3176
+ }
3177
+ }
3178
+ if (bridged > 0) {
3179
+ ctx.logger.info("Bridged objects from SchemaRegistry to metadata service", {
3180
+ count: bridged,
3181
+ total: objects.length
3182
+ });
3183
+ } else {
3184
+ ctx.logger.debug("No objects needed bridging (all already in metadata service)");
3185
+ }
3186
+ } catch (e) {
3187
+ ctx.logger.debug("Failed to bridge objects to metadata service", {
3188
+ error: e instanceof Error ? e.message : String(e)
3189
+ });
3190
+ }
3191
+ }
2940
3192
  /**
2941
3193
  * Load metadata from external metadata service into ObjectQL registry
2942
3194
  * This enables ObjectQL to use file-based or remote metadata