@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.js CHANGED
@@ -93,36 +93,45 @@ var SchemaRegistry = class {
93
93
  // ==========================================
94
94
  /**
95
95
  * Register a namespace for a package.
96
- * Enforces namespace uniqueness within the instance.
97
- *
98
- * @throws Error if namespace is already registered to a different package
96
+ * Multiple packages can share the same namespace (e.g. 'sys').
99
97
  */
100
98
  static registerNamespace(namespace, packageId) {
101
99
  if (!namespace) return;
102
- const existing = this.namespaceRegistry.get(namespace);
103
- if (existing && existing !== packageId) {
104
- throw new Error(
105
- `Namespace "${namespace}" is already registered to package "${existing}". Package "${packageId}" cannot use the same namespace.`
106
- );
100
+ let owners = this.namespaceRegistry.get(namespace);
101
+ if (!owners) {
102
+ owners = /* @__PURE__ */ new Set();
103
+ this.namespaceRegistry.set(namespace, owners);
107
104
  }
108
- this.namespaceRegistry.set(namespace, packageId);
105
+ owners.add(packageId);
109
106
  this.log(`[Registry] Registered namespace: ${namespace} \u2192 ${packageId}`);
110
107
  }
111
108
  /**
112
109
  * Unregister a namespace when a package is uninstalled.
113
110
  */
114
111
  static unregisterNamespace(namespace, packageId) {
115
- const existing = this.namespaceRegistry.get(namespace);
116
- if (existing === packageId) {
117
- this.namespaceRegistry.delete(namespace);
118
- this.log(`[Registry] Unregistered namespace: ${namespace}`);
112
+ const owners = this.namespaceRegistry.get(namespace);
113
+ if (owners) {
114
+ owners.delete(packageId);
115
+ if (owners.size === 0) {
116
+ this.namespaceRegistry.delete(namespace);
117
+ }
118
+ this.log(`[Registry] Unregistered namespace: ${namespace} \u2190 ${packageId}`);
119
119
  }
120
120
  }
121
121
  /**
122
- * Get the package that owns a namespace.
122
+ * Get the packages that use a namespace.
123
123
  */
124
124
  static getNamespaceOwner(namespace) {
125
- return this.namespaceRegistry.get(namespace);
125
+ const owners = this.namespaceRegistry.get(namespace);
126
+ if (!owners || owners.size === 0) return void 0;
127
+ return owners.values().next().value;
128
+ }
129
+ /**
130
+ * Get all packages that share a namespace.
131
+ */
132
+ static getNamespaceOwners(namespace) {
133
+ const owners = this.namespaceRegistry.get(namespace);
134
+ return owners ? Array.from(owners) : [];
126
135
  }
127
136
  // ==========================================
128
137
  // Object Registration (Ownership Model)
@@ -332,7 +341,7 @@ var SchemaRegistry = class {
332
341
  if (type === "object") {
333
342
  return import_data.ObjectSchema.parse(item);
334
343
  }
335
- if (type === "apps") {
344
+ if (type === "app") {
336
345
  return import_ui.AppSchema.parse(item);
337
346
  }
338
347
  if (type === "package") {
@@ -482,13 +491,13 @@ var SchemaRegistry = class {
482
491
  // App Helpers
483
492
  // ==========================================
484
493
  static registerApp(app, packageId) {
485
- this.registerItem("apps", app, "name", packageId);
494
+ this.registerItem("app", app, "name", packageId);
486
495
  }
487
496
  static getApp(name) {
488
- return this.getItem("apps", name);
497
+ return this.getItem("app", name);
489
498
  }
490
499
  static getAllApps() {
491
- return this.listItems("apps");
500
+ return this.listItems("app");
492
501
  }
493
502
  // ==========================================
494
503
  // Plugin Helpers
@@ -534,7 +543,7 @@ SchemaRegistry._logLevel = "info";
534
543
  SchemaRegistry.objectContributors = /* @__PURE__ */ new Map();
535
544
  /** FQN → Merged ServiceObject (cached, invalidated on changes) */
536
545
  SchemaRegistry.mergedObjectCache = /* @__PURE__ */ new Map();
537
- /** Namespace → PackageId (ensures namespace uniqueness) */
546
+ /** Namespace → Set<PackageId> (multiple packages can share a namespace) */
538
547
  SchemaRegistry.namespaceRegistry = /* @__PURE__ */ new Map();
539
548
  // ==========================================
540
549
  // Generic metadata storage (non-object types)
@@ -544,6 +553,7 @@ SchemaRegistry.metadata = /* @__PURE__ */ new Map();
544
553
 
545
554
  // src/protocol.ts
546
555
  var import_data2 = require("@objectstack/spec/data");
556
+ var import_shared = require("@objectstack/spec/shared");
547
557
  function simpleHash(str) {
548
558
  let hash = 0;
549
559
  for (let i = 0; i < str.length; i++) {
@@ -671,20 +681,32 @@ var ObjectStackProtocolImplementation = class {
671
681
  };
672
682
  }
673
683
  async getMetaTypes() {
674
- return {
675
- types: SchemaRegistry.getRegisteredTypes()
676
- };
684
+ const schemaTypes = SchemaRegistry.getRegisteredTypes();
685
+ let runtimeTypes = [];
686
+ try {
687
+ const services = this.getServicesRegistry?.();
688
+ const metadataService = services?.get("metadata");
689
+ if (metadataService && typeof metadataService.getRegisteredTypes === "function") {
690
+ runtimeTypes = await metadataService.getRegisteredTypes();
691
+ }
692
+ } catch {
693
+ }
694
+ const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
695
+ return { types: allTypes };
677
696
  }
678
697
  async getMetaItems(request) {
679
- let items = SchemaRegistry.listItems(request.type);
698
+ const { packageId } = request;
699
+ let items = SchemaRegistry.listItems(request.type, packageId);
680
700
  if (items.length === 0) {
681
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
682
- items = SchemaRegistry.listItems(alt);
701
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
702
+ if (alt) items = SchemaRegistry.listItems(alt, packageId);
683
703
  }
684
704
  if (items.length === 0) {
685
705
  try {
706
+ const whereClause = { type: request.type, state: "active" };
707
+ if (packageId) whereClause._packageId = packageId;
686
708
  const allRecords = await this.engine.find("sys_metadata", {
687
- where: { type: request.type, state: "active" }
709
+ where: whereClause
688
710
  });
689
711
  if (allRecords && allRecords.length > 0) {
690
712
  items = allRecords.map((record) => {
@@ -693,21 +715,47 @@ var ObjectStackProtocolImplementation = class {
693
715
  return data;
694
716
  });
695
717
  } else {
696
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
697
- const altRecords = await this.engine.find("sys_metadata", {
698
- where: { type: alt, state: "active" }
699
- });
700
- if (altRecords && altRecords.length > 0) {
701
- items = altRecords.map((record) => {
702
- const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
703
- SchemaRegistry.registerItem(request.type, data, "name");
704
- return data;
718
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
719
+ if (alt) {
720
+ const altRecords = await this.engine.find("sys_metadata", {
721
+ where: { type: alt, state: "active" }
705
722
  });
723
+ if (altRecords && altRecords.length > 0) {
724
+ items = altRecords.map((record) => {
725
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
726
+ SchemaRegistry.registerItem(request.type, data, "name");
727
+ return data;
728
+ });
729
+ }
706
730
  }
707
731
  }
708
732
  } catch {
709
733
  }
710
734
  }
735
+ try {
736
+ const services = this.getServicesRegistry?.();
737
+ const metadataService = services?.get("metadata");
738
+ if (metadataService && typeof metadataService.list === "function") {
739
+ const runtimeItems = await metadataService.list(request.type);
740
+ if (runtimeItems && runtimeItems.length > 0) {
741
+ const itemMap = /* @__PURE__ */ new Map();
742
+ for (const item of items) {
743
+ const entry = item;
744
+ if (entry && typeof entry === "object" && "name" in entry) {
745
+ itemMap.set(entry.name, entry);
746
+ }
747
+ }
748
+ for (const item of runtimeItems) {
749
+ const entry = item;
750
+ if (entry && typeof entry === "object" && "name" in entry) {
751
+ itemMap.set(entry.name, entry);
752
+ }
753
+ }
754
+ items = Array.from(itemMap.values());
755
+ }
756
+ }
757
+ } catch {
758
+ }
711
759
  return {
712
760
  type: request.type,
713
761
  items
@@ -716,8 +764,8 @@ var ObjectStackProtocolImplementation = class {
716
764
  async getMetaItem(request) {
717
765
  let item = SchemaRegistry.getItem(request.type, request.name);
718
766
  if (item === void 0) {
719
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
720
- item = SchemaRegistry.getItem(alt, request.name);
767
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
768
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
721
769
  }
722
770
  if (item === void 0) {
723
771
  try {
@@ -728,18 +776,30 @@ var ObjectStackProtocolImplementation = class {
728
776
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
729
777
  SchemaRegistry.registerItem(request.type, item, "name");
730
778
  } else {
731
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
732
- const altRecord = await this.engine.findOne("sys_metadata", {
733
- where: { type: alt, name: request.name, state: "active" }
734
- });
735
- if (altRecord) {
736
- item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
737
- SchemaRegistry.registerItem(request.type, item, "name");
779
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
780
+ if (alt) {
781
+ const altRecord = await this.engine.findOne("sys_metadata", {
782
+ where: { type: alt, name: request.name, state: "active" }
783
+ });
784
+ if (altRecord) {
785
+ item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
786
+ SchemaRegistry.registerItem(request.type, item, "name");
787
+ }
738
788
  }
739
789
  }
740
790
  } catch {
741
791
  }
742
792
  }
793
+ if (item === void 0) {
794
+ try {
795
+ const services = this.getServicesRegistry?.();
796
+ const metadataService = services?.get("metadata");
797
+ if (metadataService && typeof metadataService.get === "function") {
798
+ item = await metadataService.get(request.type, request.name);
799
+ }
800
+ } catch {
801
+ }
802
+ }
743
803
  return {
744
804
  type: request.type,
745
805
  name: request.name,
@@ -912,10 +972,7 @@ var ObjectStackProtocolImplementation = class {
912
972
  const records = await this.engine.find(request.object, options);
913
973
  return {
914
974
  object: request.object,
915
- value: records,
916
- // OData compatibility
917
975
  records,
918
- // Legacy
919
976
  total: records.length,
920
977
  hasMore: false
921
978
  };
@@ -973,7 +1030,21 @@ var ObjectStackProtocolImplementation = class {
973
1030
  // ==========================================
974
1031
  async getMetaItemCached(request) {
975
1032
  try {
976
- const item = SchemaRegistry.getItem(request.type, request.name);
1033
+ let item = SchemaRegistry.getItem(request.type, request.name);
1034
+ if (!item) {
1035
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1036
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
1037
+ }
1038
+ if (!item) {
1039
+ try {
1040
+ const services = this.getServicesRegistry?.();
1041
+ const metadataService = services?.get("metadata");
1042
+ if (metadataService && typeof metadataService.get === "function") {
1043
+ item = await metadataService.get(request.type, request.name);
1044
+ }
1045
+ } catch {
1046
+ }
1047
+ }
977
1048
  if (!item) {
978
1049
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
979
1050
  }
@@ -1325,10 +1396,11 @@ var ObjectStackProtocolImplementation = class {
1325
1396
  for (const record of records) {
1326
1397
  try {
1327
1398
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1328
- if (record.type === "object") {
1399
+ const normalizedType = import_shared.PLURAL_TO_SINGULAR[record.type] ?? record.type;
1400
+ if (normalizedType === "object") {
1329
1401
  SchemaRegistry.registerObject(data, record.packageId || "sys_metadata");
1330
1402
  } else {
1331
- SchemaRegistry.registerItem(record.type, data, "name");
1403
+ SchemaRegistry.registerItem(normalizedType, data, "name");
1332
1404
  }
1333
1405
  loaded++;
1334
1406
  } catch (e) {
@@ -1478,6 +1550,7 @@ var ObjectStackProtocolImplementation = class {
1478
1550
  var import_kernel2 = require("@objectstack/spec/kernel");
1479
1551
  var import_core = require("@objectstack/core");
1480
1552
  var import_system = require("@objectstack/spec/system");
1553
+ var import_shared2 = require("@objectstack/spec/shared");
1481
1554
  var _ObjectQL = class _ObjectQL {
1482
1555
  constructor(hostContext = {}) {
1483
1556
  this.drivers = /* @__PURE__ */ new Map();
@@ -1768,7 +1841,7 @@ var _ObjectQL = class _ObjectQL {
1768
1841
  for (const item of items) {
1769
1842
  const itemName = item.name || item.id;
1770
1843
  if (itemName) {
1771
- SchemaRegistry.registerItem(key, item, "name", id);
1844
+ SchemaRegistry.registerItem((0, import_shared2.pluralToSingular)(key), item, "name", id);
1772
1845
  }
1773
1846
  }
1774
1847
  }
@@ -1874,7 +1947,7 @@ var _ObjectQL = class _ObjectQL {
1874
1947
  for (const item of items) {
1875
1948
  const itemName = item.name || item.id;
1876
1949
  if (itemName) {
1877
- SchemaRegistry.registerItem(key, item, "name", ownerId);
1950
+ SchemaRegistry.registerItem((0, import_shared2.pluralToSingular)(key), item, "name", ownerId);
1878
1951
  }
1879
1952
  }
1880
1953
  }
@@ -1898,6 +1971,16 @@ var _ObjectQL = class _ObjectQL {
1898
1971
  this.logger.info("Set default driver", { driverName: driver.name });
1899
1972
  }
1900
1973
  }
1974
+ /**
1975
+ * Set the realtime service for publishing data change events.
1976
+ * Should be called after kernel resolves the realtime service.
1977
+ *
1978
+ * @param service - An IRealtimeService instance for event publishing
1979
+ */
1980
+ setRealtimeService(service) {
1981
+ this.realtimeService = service;
1982
+ this.logger.info("RealtimeService configured for data events");
1983
+ }
1901
1984
  /**
1902
1985
  * Helper to get object definition
1903
1986
  */
@@ -1952,14 +2035,22 @@ var _ObjectQL = class _ObjectQL {
1952
2035
  driverCount: this.drivers.size,
1953
2036
  drivers: Array.from(this.drivers.keys())
1954
2037
  });
2038
+ const failedDrivers = [];
1955
2039
  for (const [name, driver] of this.drivers) {
1956
2040
  try {
1957
2041
  await driver.connect();
1958
2042
  this.logger.info("Driver connected successfully", { driverName: name });
1959
2043
  } catch (e) {
2044
+ failedDrivers.push(name);
1960
2045
  this.logger.error("Failed to connect driver", e, { driverName: name });
1961
2046
  }
1962
2047
  }
2048
+ if (failedDrivers.length > 0) {
2049
+ this.logger.warn(
2050
+ `${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. Operations may recover via lazy reconnection or fail at query time.`,
2051
+ { failedDrivers }
2052
+ );
2053
+ }
1963
2054
  this.logger.info("ObjectQL engine initialization complete");
1964
2055
  }
1965
2056
  async destroy() {
@@ -2161,6 +2252,39 @@ var _ObjectQL = class _ObjectQL {
2161
2252
  hookContext.event = "afterInsert";
2162
2253
  hookContext.result = result;
2163
2254
  await this.triggerHooks("afterInsert", hookContext);
2255
+ if (this.realtimeService) {
2256
+ try {
2257
+ if (Array.isArray(result)) {
2258
+ for (const record of result) {
2259
+ const event = {
2260
+ type: "data.record.created",
2261
+ object,
2262
+ payload: {
2263
+ recordId: record.id,
2264
+ after: record
2265
+ },
2266
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2267
+ };
2268
+ await this.realtimeService.publish(event);
2269
+ }
2270
+ this.logger.debug(`Published ${result.length} data.record.created events`, { object });
2271
+ } else {
2272
+ const event = {
2273
+ type: "data.record.created",
2274
+ object,
2275
+ payload: {
2276
+ recordId: result.id,
2277
+ after: result
2278
+ },
2279
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2280
+ };
2281
+ await this.realtimeService.publish(event);
2282
+ this.logger.debug("Published data.record.created event", { object, recordId: result.id });
2283
+ }
2284
+ } catch (error) {
2285
+ this.logger.warn("Failed to publish data event", { object, error });
2286
+ }
2287
+ }
2164
2288
  return hookContext.result;
2165
2289
  } catch (e) {
2166
2290
  this.logger.error("Insert operation failed", e, { object });
@@ -2207,6 +2331,26 @@ var _ObjectQL = class _ObjectQL {
2207
2331
  hookContext.event = "afterUpdate";
2208
2332
  hookContext.result = result;
2209
2333
  await this.triggerHooks("afterUpdate", hookContext);
2334
+ if (this.realtimeService) {
2335
+ try {
2336
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2337
+ const recordId = String(hookContext.input.id || resultId || "");
2338
+ const event = {
2339
+ type: "data.record.updated",
2340
+ object,
2341
+ payload: {
2342
+ recordId,
2343
+ changes: hookContext.input.data,
2344
+ after: result
2345
+ },
2346
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2347
+ };
2348
+ await this.realtimeService.publish(event);
2349
+ this.logger.debug("Published data.record.updated event", { object, recordId });
2350
+ } catch (error) {
2351
+ this.logger.warn("Failed to publish data event", { object, error });
2352
+ }
2353
+ }
2210
2354
  return hookContext.result;
2211
2355
  } catch (e) {
2212
2356
  this.logger.error("Update operation failed", e, { object });
@@ -2252,6 +2396,24 @@ var _ObjectQL = class _ObjectQL {
2252
2396
  hookContext.event = "afterDelete";
2253
2397
  hookContext.result = result;
2254
2398
  await this.triggerHooks("afterDelete", hookContext);
2399
+ if (this.realtimeService) {
2400
+ try {
2401
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2402
+ const recordId = String(hookContext.input.id || resultId || "");
2403
+ const event = {
2404
+ type: "data.record.deleted",
2405
+ object,
2406
+ payload: {
2407
+ recordId
2408
+ },
2409
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2410
+ };
2411
+ await this.realtimeService.publish(event);
2412
+ this.logger.debug("Published data.record.deleted event", { object, recordId });
2413
+ } catch (error) {
2414
+ this.logger.warn("Failed to publish data event", { object, error });
2415
+ }
2416
+ }
2255
2417
  return hookContext.result;
2256
2418
  } catch (e) {
2257
2419
  this.logger.error("Delete operation failed", e, { object });
@@ -2709,6 +2871,9 @@ var MetadataFacade = class {
2709
2871
  };
2710
2872
 
2711
2873
  // src/plugin.ts
2874
+ function hasLoadMetaFromDb(service) {
2875
+ return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
2876
+ }
2712
2877
  var ObjectQLPlugin = class {
2713
2878
  constructor(ql, hostContext) {
2714
2879
  this.name = "com.objectstack.engine.objectql";
@@ -2720,38 +2885,18 @@ var ObjectQLPlugin = class {
2720
2885
  this.ql = new ObjectQL(hostCtx);
2721
2886
  }
2722
2887
  ctx.registerService("objectql", this.ql);
2723
- let hasMetadata = false;
2724
- let metadataProvider = "objectql";
2725
- try {
2726
- if (ctx.getService("metadata")) {
2727
- hasMetadata = true;
2728
- metadataProvider = "external";
2729
- }
2730
- } catch (e) {
2731
- }
2732
- if (!hasMetadata) {
2733
- try {
2734
- const metadataFacade = new MetadataFacade();
2735
- ctx.registerService("metadata", metadataFacade);
2736
- ctx.logger.info("MetadataFacade registered as metadata service", {
2737
- mode: "in-memory",
2738
- features: ["registry", "fast-lookup"]
2888
+ ctx.registerService("data", this.ql);
2889
+ const ql = this.ql;
2890
+ ctx.registerService("manifest", {
2891
+ register: (manifest) => {
2892
+ ql.registerApp(manifest);
2893
+ ctx.logger.debug("Manifest registered via manifest service", {
2894
+ id: manifest.id || manifest.name
2739
2895
  });
2740
- } catch (e) {
2741
- if (!e.message?.includes("already registered")) {
2742
- throw e;
2743
- }
2744
2896
  }
2745
- } else {
2746
- ctx.logger.info("External metadata service detected", {
2747
- provider: metadataProvider,
2748
- mode: "will-sync-in-start-phase"
2749
- });
2750
- }
2751
- ctx.registerService("data", this.ql);
2897
+ });
2752
2898
  ctx.logger.info("ObjectQL engine registered", {
2753
- services: ["objectql", "data"],
2754
- metadataProvider
2899
+ services: ["objectql", "data", "manifest"]
2755
2900
  });
2756
2901
  const protocolShim = new ObjectStackProtocolImplementation(
2757
2902
  this.ql,
@@ -2764,7 +2909,7 @@ var ObjectQLPlugin = class {
2764
2909
  ctx.logger.info("ObjectQL engine starting...");
2765
2910
  try {
2766
2911
  const metadataService = ctx.getService("metadata");
2767
- if (metadataService && !(metadataService instanceof MetadataFacade) && this.ql) {
2912
+ if (metadataService && typeof metadataService.loadMany === "function" && this.ql) {
2768
2913
  await this.loadMetadataFromService(metadataService, ctx);
2769
2914
  }
2770
2915
  } catch (e) {
@@ -2778,13 +2923,29 @@ var ObjectQLPlugin = class {
2778
2923
  ctx.logger.debug("Discovered and registered driver service", { serviceName: name });
2779
2924
  }
2780
2925
  if (name.startsWith("app.")) {
2926
+ ctx.logger.warn(
2927
+ `[DEPRECATED] Service "${name}" uses legacy app.* convention. Migrate to ctx.getService('manifest').register(data).`
2928
+ );
2781
2929
  this.ql.registerApp(service);
2782
- ctx.logger.debug("Discovered and registered app service", { serviceName: name });
2930
+ ctx.logger.debug("Discovered and registered app service (legacy)", { serviceName: name });
2931
+ }
2932
+ }
2933
+ try {
2934
+ const realtimeService = ctx.getService("realtime");
2935
+ if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
2936
+ ctx.logger.info("[ObjectQLPlugin] Bridging realtime service to ObjectQL for event publishing");
2937
+ this.ql.setRealtimeService(realtimeService);
2783
2938
  }
2939
+ } catch (e) {
2940
+ ctx.logger.debug("[ObjectQLPlugin] No realtime service found \u2014 data events will not be published", {
2941
+ error: e.message
2942
+ });
2784
2943
  }
2785
2944
  }
2786
2945
  await this.ql?.init();
2946
+ await this.restoreMetadataFromDb(ctx);
2787
2947
  await this.syncRegisteredSchemas(ctx);
2948
+ await this.bridgeObjectsToMetadataService(ctx);
2788
2949
  this.registerAuditHooks(ctx);
2789
2950
  this.registerTenantMiddleware(ctx);
2790
2951
  ctx.logger.info("ObjectQL engine started", {
@@ -2977,6 +3138,97 @@ var ObjectQLPlugin = class {
2977
3138
  ctx.logger.info("Schema sync complete", { synced, skipped, total: allObjects.length });
2978
3139
  }
2979
3140
  }
3141
+ /**
3142
+ * Restore persisted metadata from the database (sys_metadata) on startup.
3143
+ *
3144
+ * Calls `protocol.loadMetaFromDb()` to bulk-load all active metadata
3145
+ * records (objects, views, apps, etc.) into the in-memory SchemaRegistry.
3146
+ * This closes the persistence loop so that user-created schemas survive
3147
+ * kernel cold starts and redeployments.
3148
+ *
3149
+ * Gracefully degrades when:
3150
+ * - The protocol service is unavailable (e.g., in-memory-only mode).
3151
+ * - `loadMetaFromDb` is not implemented by the protocol shim.
3152
+ * - The underlying driver/table does not exist yet (first-run scenario).
3153
+ */
3154
+ async restoreMetadataFromDb(ctx) {
3155
+ let protocol;
3156
+ try {
3157
+ const service = ctx.getService("protocol");
3158
+ if (!service || !hasLoadMetaFromDb(service)) {
3159
+ ctx.logger.debug("Protocol service does not support loadMetaFromDb, skipping DB restore");
3160
+ return;
3161
+ }
3162
+ protocol = service;
3163
+ } catch (e) {
3164
+ ctx.logger.debug("Protocol service unavailable, skipping DB restore", {
3165
+ error: e instanceof Error ? e.message : String(e)
3166
+ });
3167
+ return;
3168
+ }
3169
+ try {
3170
+ const { loaded, errors } = await protocol.loadMetaFromDb();
3171
+ if (loaded > 0 || errors > 0) {
3172
+ ctx.logger.info("Metadata restored from database to SchemaRegistry", { loaded, errors });
3173
+ } else {
3174
+ ctx.logger.debug("No persisted metadata found in database");
3175
+ }
3176
+ } catch (e) {
3177
+ ctx.logger.debug("DB metadata restore failed (non-fatal)", {
3178
+ error: e instanceof Error ? e.message : String(e)
3179
+ });
3180
+ }
3181
+ }
3182
+ /**
3183
+ * Bridge all SchemaRegistry objects to the metadata service.
3184
+ *
3185
+ * This ensures objects registered by plugins and loaded from sys_metadata
3186
+ * are visible to AI tools and other consumers that query IMetadataService.
3187
+ *
3188
+ * Runs after both restoreMetadataFromDb() and syncRegisteredSchemas() to
3189
+ * catch all objects in the SchemaRegistry regardless of their source.
3190
+ */
3191
+ async bridgeObjectsToMetadataService(ctx) {
3192
+ try {
3193
+ const metadataService = ctx.getService("metadata");
3194
+ if (!metadataService || typeof metadataService.register !== "function") {
3195
+ ctx.logger.debug("Metadata service unavailable for bridging, skipping");
3196
+ return;
3197
+ }
3198
+ if (!this.ql?.registry) {
3199
+ ctx.logger.debug("SchemaRegistry unavailable for bridging, skipping");
3200
+ return;
3201
+ }
3202
+ const objects = this.ql.registry.getAllObjects();
3203
+ let bridged = 0;
3204
+ for (const obj of objects) {
3205
+ try {
3206
+ const existing = await metadataService.getObject(obj.name);
3207
+ if (!existing) {
3208
+ await metadataService.register("object", obj.name, obj);
3209
+ bridged++;
3210
+ }
3211
+ } catch (e) {
3212
+ ctx.logger.debug("Failed to bridge object to metadata service", {
3213
+ object: obj.name,
3214
+ error: e instanceof Error ? e.message : String(e)
3215
+ });
3216
+ }
3217
+ }
3218
+ if (bridged > 0) {
3219
+ ctx.logger.info("Bridged objects from SchemaRegistry to metadata service", {
3220
+ count: bridged,
3221
+ total: objects.length
3222
+ });
3223
+ } else {
3224
+ ctx.logger.debug("No objects needed bridging (all already in metadata service)");
3225
+ }
3226
+ } catch (e) {
3227
+ ctx.logger.debug("Failed to bridge objects to metadata service", {
3228
+ error: e instanceof Error ? e.message : String(e)
3229
+ });
3230
+ }
3231
+ }
2980
3232
  /**
2981
3233
  * Load metadata from external metadata service into ObjectQL registry
2982
3234
  * This enables ObjectQL to use file-based or remote metadata