@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.
package/dist/index.js CHANGED
@@ -341,7 +341,7 @@ var SchemaRegistry = class {
341
341
  if (type === "object") {
342
342
  return import_data.ObjectSchema.parse(item);
343
343
  }
344
- if (type === "apps") {
344
+ if (type === "app") {
345
345
  return import_ui.AppSchema.parse(item);
346
346
  }
347
347
  if (type === "package") {
@@ -491,13 +491,13 @@ var SchemaRegistry = class {
491
491
  // App Helpers
492
492
  // ==========================================
493
493
  static registerApp(app, packageId) {
494
- this.registerItem("apps", app, "name", packageId);
494
+ this.registerItem("app", app, "name", packageId);
495
495
  }
496
496
  static getApp(name) {
497
- return this.getItem("apps", name);
497
+ return this.getItem("app", name);
498
498
  }
499
499
  static getAllApps() {
500
- return this.listItems("apps");
500
+ return this.listItems("app");
501
501
  }
502
502
  // ==========================================
503
503
  // Plugin Helpers
@@ -553,6 +553,7 @@ SchemaRegistry.metadata = /* @__PURE__ */ new Map();
553
553
 
554
554
  // src/protocol.ts
555
555
  var import_data2 = require("@objectstack/spec/data");
556
+ var import_shared = require("@objectstack/spec/shared");
556
557
  function simpleHash(str) {
557
558
  let hash = 0;
558
559
  for (let i = 0; i < str.length; i++) {
@@ -680,16 +681,25 @@ var ObjectStackProtocolImplementation = class {
680
681
  };
681
682
  }
682
683
  async getMetaTypes() {
683
- return {
684
- types: SchemaRegistry.getRegisteredTypes()
685
- };
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 };
686
696
  }
687
697
  async getMetaItems(request) {
688
698
  const { packageId } = request;
689
699
  let items = SchemaRegistry.listItems(request.type, packageId);
690
700
  if (items.length === 0) {
691
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
692
- items = SchemaRegistry.listItems(alt, packageId);
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);
693
703
  }
694
704
  if (items.length === 0) {
695
705
  try {
@@ -705,21 +715,50 @@ var ObjectStackProtocolImplementation = class {
705
715
  return data;
706
716
  });
707
717
  } else {
708
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
709
- const altRecords = await this.engine.find("sys_metadata", {
710
- where: { type: alt, state: "active" }
711
- });
712
- if (altRecords && altRecords.length > 0) {
713
- items = altRecords.map((record) => {
714
- const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
715
- SchemaRegistry.registerItem(request.type, data, "name");
716
- 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" }
717
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
+ }
718
730
  }
719
731
  }
720
732
  } catch {
721
733
  }
722
734
  }
735
+ try {
736
+ const services = this.getServicesRegistry?.();
737
+ const metadataService = services?.get("metadata");
738
+ if (metadataService && typeof metadataService.list === "function") {
739
+ let runtimeItems = await metadataService.list(request.type);
740
+ if (packageId && runtimeItems && runtimeItems.length > 0) {
741
+ runtimeItems = runtimeItems.filter((item) => item?._packageId === packageId);
742
+ }
743
+ if (runtimeItems && runtimeItems.length > 0) {
744
+ const itemMap = /* @__PURE__ */ new Map();
745
+ for (const item of items) {
746
+ const entry = item;
747
+ if (entry && typeof entry === "object" && "name" in entry) {
748
+ itemMap.set(entry.name, entry);
749
+ }
750
+ }
751
+ for (const item of runtimeItems) {
752
+ const entry = item;
753
+ if (entry && typeof entry === "object" && "name" in entry) {
754
+ itemMap.set(entry.name, entry);
755
+ }
756
+ }
757
+ items = Array.from(itemMap.values());
758
+ }
759
+ }
760
+ } catch {
761
+ }
723
762
  return {
724
763
  type: request.type,
725
764
  items
@@ -728,8 +767,8 @@ var ObjectStackProtocolImplementation = class {
728
767
  async getMetaItem(request) {
729
768
  let item = SchemaRegistry.getItem(request.type, request.name);
730
769
  if (item === void 0) {
731
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
732
- item = SchemaRegistry.getItem(alt, request.name);
770
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
771
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
733
772
  }
734
773
  if (item === void 0) {
735
774
  try {
@@ -740,18 +779,30 @@ var ObjectStackProtocolImplementation = class {
740
779
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
741
780
  SchemaRegistry.registerItem(request.type, item, "name");
742
781
  } else {
743
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
744
- const altRecord = await this.engine.findOne("sys_metadata", {
745
- where: { type: alt, name: request.name, state: "active" }
746
- });
747
- if (altRecord) {
748
- item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
749
- SchemaRegistry.registerItem(request.type, item, "name");
782
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
783
+ if (alt) {
784
+ const altRecord = await this.engine.findOne("sys_metadata", {
785
+ where: { type: alt, name: request.name, state: "active" }
786
+ });
787
+ if (altRecord) {
788
+ item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
789
+ SchemaRegistry.registerItem(request.type, item, "name");
790
+ }
750
791
  }
751
792
  }
752
793
  } catch {
753
794
  }
754
795
  }
796
+ if (item === void 0) {
797
+ try {
798
+ const services = this.getServicesRegistry?.();
799
+ const metadataService = services?.get("metadata");
800
+ if (metadataService && typeof metadataService.get === "function") {
801
+ item = await metadataService.get(request.type, request.name);
802
+ }
803
+ } catch {
804
+ }
805
+ }
755
806
  return {
756
807
  type: request.type,
757
808
  name: request.name,
@@ -982,7 +1033,21 @@ var ObjectStackProtocolImplementation = class {
982
1033
  // ==========================================
983
1034
  async getMetaItemCached(request) {
984
1035
  try {
985
- const item = SchemaRegistry.getItem(request.type, request.name);
1036
+ let item = SchemaRegistry.getItem(request.type, request.name);
1037
+ if (!item) {
1038
+ const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1039
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
1040
+ }
1041
+ if (!item) {
1042
+ try {
1043
+ const services = this.getServicesRegistry?.();
1044
+ const metadataService = services?.get("metadata");
1045
+ if (metadataService && typeof metadataService.get === "function") {
1046
+ item = await metadataService.get(request.type, request.name);
1047
+ }
1048
+ } catch {
1049
+ }
1050
+ }
986
1051
  if (!item) {
987
1052
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
988
1053
  }
@@ -1334,10 +1399,11 @@ var ObjectStackProtocolImplementation = class {
1334
1399
  for (const record of records) {
1335
1400
  try {
1336
1401
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1337
- if (record.type === "object") {
1402
+ const normalizedType = import_shared.PLURAL_TO_SINGULAR[record.type] ?? record.type;
1403
+ if (normalizedType === "object") {
1338
1404
  SchemaRegistry.registerObject(data, record.packageId || "sys_metadata");
1339
1405
  } else {
1340
- SchemaRegistry.registerItem(record.type, data, "name");
1406
+ SchemaRegistry.registerItem(normalizedType, data, "name");
1341
1407
  }
1342
1408
  loaded++;
1343
1409
  } catch (e) {
@@ -1487,10 +1553,15 @@ var ObjectStackProtocolImplementation = class {
1487
1553
  var import_kernel2 = require("@objectstack/spec/kernel");
1488
1554
  var import_core = require("@objectstack/core");
1489
1555
  var import_system = require("@objectstack/spec/system");
1556
+ var import_shared2 = require("@objectstack/spec/shared");
1490
1557
  var _ObjectQL = class _ObjectQL {
1491
1558
  constructor(hostContext = {}) {
1492
1559
  this.drivers = /* @__PURE__ */ new Map();
1493
1560
  this.defaultDriver = null;
1561
+ // Datasource mapping rules (imported from defineStack)
1562
+ this.datasourceMapping = [];
1563
+ // Package manifests registry (for defaultDatasource lookup)
1564
+ this.manifests = /* @__PURE__ */ new Map();
1494
1565
  // Per-object hooks with priority support
1495
1566
  this.hooks = /* @__PURE__ */ new Map([
1496
1567
  ["beforeFind", []],
@@ -1688,6 +1759,9 @@ var _ObjectQL = class _ObjectQL {
1688
1759
  const id = manifest.id || manifest.name;
1689
1760
  const namespace = manifest.namespace;
1690
1761
  this.logger.debug("Registering package manifest", { id, namespace });
1762
+ if (id) {
1763
+ this.manifests.set(id, manifest);
1764
+ }
1691
1765
  SchemaRegistry.installPackage(manifest);
1692
1766
  this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
1693
1767
  if (manifest.objects) {
@@ -1777,7 +1851,7 @@ var _ObjectQL = class _ObjectQL {
1777
1851
  for (const item of items) {
1778
1852
  const itemName = item.name || item.id;
1779
1853
  if (itemName) {
1780
- SchemaRegistry.registerItem(key, item, "name", id);
1854
+ SchemaRegistry.registerItem((0, import_shared2.pluralToSingular)(key), item, "name", id);
1781
1855
  }
1782
1856
  }
1783
1857
  }
@@ -1883,7 +1957,7 @@ var _ObjectQL = class _ObjectQL {
1883
1957
  for (const item of items) {
1884
1958
  const itemName = item.name || item.id;
1885
1959
  if (itemName) {
1886
- SchemaRegistry.registerItem(key, item, "name", ownerId);
1960
+ SchemaRegistry.registerItem((0, import_shared2.pluralToSingular)(key), item, "name", ownerId);
1887
1961
  }
1888
1962
  }
1889
1963
  }
@@ -1907,6 +1981,16 @@ var _ObjectQL = class _ObjectQL {
1907
1981
  this.logger.info("Set default driver", { driverName: driver.name });
1908
1982
  }
1909
1983
  }
1984
+ /**
1985
+ * Set the realtime service for publishing data change events.
1986
+ * Should be called after kernel resolves the realtime service.
1987
+ *
1988
+ * @param service - An IRealtimeService instance for event publishing
1989
+ */
1990
+ setRealtimeService(service) {
1991
+ this.realtimeService = service;
1992
+ this.logger.info("RealtimeService configured for data events");
1993
+ }
1910
1994
  /**
1911
1995
  * Helper to get object definition
1912
1996
  */
@@ -1932,27 +2016,99 @@ var _ObjectQL = class _ObjectQL {
1932
2016
  }
1933
2017
  /**
1934
2018
  * Helper to get the target driver
2019
+ *
2020
+ * Resolution priority (first match wins):
2021
+ * 1. Object's explicit `datasource` field (if not 'default')
2022
+ * 2. DatasourceMapping rules (namespace/package/pattern matching)
2023
+ * 3. Package's `defaultDatasource` from manifest
2024
+ * 4. Global default driver
1935
2025
  */
1936
2026
  getDriver(objectName) {
1937
2027
  const object = SchemaRegistry.getObject(objectName);
1938
- if (object) {
1939
- const datasourceName = object.datasource || "default";
1940
- if (datasourceName === "default") {
1941
- if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
1942
- return this.drivers.get(this.defaultDriver);
1943
- }
1944
- } else {
1945
- if (this.drivers.has(datasourceName)) {
1946
- return this.drivers.get(datasourceName);
2028
+ if (object?.datasource && object.datasource !== "default") {
2029
+ if (this.drivers.has(object.datasource)) {
2030
+ return this.drivers.get(object.datasource);
2031
+ }
2032
+ throw new Error(`[ObjectQL] Datasource '${object.datasource}' configured for object '${objectName}' is not registered.`);
2033
+ }
2034
+ const mappedDatasource = this.resolveDatasourceFromMapping(objectName, object);
2035
+ if (mappedDatasource && this.drivers.has(mappedDatasource)) {
2036
+ this.logger.debug("Resolved datasource from mapping", {
2037
+ object: objectName,
2038
+ datasource: mappedDatasource
2039
+ });
2040
+ return this.drivers.get(mappedDatasource);
2041
+ }
2042
+ const fqn = object?.name || objectName;
2043
+ const owner = SchemaRegistry.getObjectOwner(fqn);
2044
+ if (owner?.packageId) {
2045
+ const manifest = this.manifests.get(owner.packageId);
2046
+ if (manifest?.defaultDatasource && manifest.defaultDatasource !== "default") {
2047
+ if (this.drivers.has(manifest.defaultDatasource)) {
2048
+ this.logger.debug("Resolved datasource from package manifest", {
2049
+ object: objectName,
2050
+ package: owner.packageId,
2051
+ datasource: manifest.defaultDatasource
2052
+ });
2053
+ return this.drivers.get(manifest.defaultDatasource);
1947
2054
  }
1948
- throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
1949
2055
  }
1950
2056
  }
1951
- if (this.defaultDriver) {
2057
+ if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
1952
2058
  return this.drivers.get(this.defaultDriver);
1953
2059
  }
1954
2060
  throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
1955
2061
  }
2062
+ /**
2063
+ * Resolve datasource from mapping rules
2064
+ *
2065
+ * Rules are evaluated in order (or by priority if specified).
2066
+ * First matching rule wins.
2067
+ */
2068
+ resolveDatasourceFromMapping(objectName, object) {
2069
+ if (!this.datasourceMapping || this.datasourceMapping.length === 0) {
2070
+ return null;
2071
+ }
2072
+ const sortedRules = [...this.datasourceMapping].sort((a, b) => {
2073
+ const aPriority = a.priority ?? 1e3;
2074
+ const bPriority = b.priority ?? 1e3;
2075
+ return aPriority - bPriority;
2076
+ });
2077
+ for (const rule of sortedRules) {
2078
+ if (rule.namespace && object?.namespace === rule.namespace) {
2079
+ return rule.datasource;
2080
+ }
2081
+ if (rule.package && object?.packageId === rule.package) {
2082
+ return rule.datasource;
2083
+ }
2084
+ if (rule.objectPattern && this.matchPattern(objectName, rule.objectPattern)) {
2085
+ return rule.datasource;
2086
+ }
2087
+ if (rule.default) {
2088
+ return rule.datasource;
2089
+ }
2090
+ }
2091
+ return null;
2092
+ }
2093
+ /**
2094
+ * Simple glob pattern matching
2095
+ * Supports * (any chars) and ? (single char)
2096
+ */
2097
+ matchPattern(objectName, pattern) {
2098
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2099
+ const regex = new RegExp(`^${regexPattern}$`);
2100
+ return regex.test(objectName);
2101
+ }
2102
+ /**
2103
+ * Set datasource mapping rules
2104
+ * Called by ObjectQLPlugin during bootstrap
2105
+ */
2106
+ setDatasourceMapping(rules) {
2107
+ this.datasourceMapping = rules;
2108
+ this.logger.info("Datasource mapping rules configured", {
2109
+ ruleCount: rules.length
2110
+ });
2111
+ }
1956
2112
  /**
1957
2113
  * Initialize the engine and all registered drivers
1958
2114
  */
@@ -1961,14 +2117,22 @@ var _ObjectQL = class _ObjectQL {
1961
2117
  driverCount: this.drivers.size,
1962
2118
  drivers: Array.from(this.drivers.keys())
1963
2119
  });
2120
+ const failedDrivers = [];
1964
2121
  for (const [name, driver] of this.drivers) {
1965
2122
  try {
1966
2123
  await driver.connect();
1967
2124
  this.logger.info("Driver connected successfully", { driverName: name });
1968
2125
  } catch (e) {
2126
+ failedDrivers.push(name);
1969
2127
  this.logger.error("Failed to connect driver", e, { driverName: name });
1970
2128
  }
1971
2129
  }
2130
+ if (failedDrivers.length > 0) {
2131
+ this.logger.warn(
2132
+ `${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. Operations may recover via lazy reconnection or fail at query time.`,
2133
+ { failedDrivers }
2134
+ );
2135
+ }
1972
2136
  this.logger.info("ObjectQL engine initialization complete");
1973
2137
  }
1974
2138
  async destroy() {
@@ -2170,6 +2334,39 @@ var _ObjectQL = class _ObjectQL {
2170
2334
  hookContext.event = "afterInsert";
2171
2335
  hookContext.result = result;
2172
2336
  await this.triggerHooks("afterInsert", hookContext);
2337
+ if (this.realtimeService) {
2338
+ try {
2339
+ if (Array.isArray(result)) {
2340
+ for (const record of result) {
2341
+ const event = {
2342
+ type: "data.record.created",
2343
+ object,
2344
+ payload: {
2345
+ recordId: record.id,
2346
+ after: record
2347
+ },
2348
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2349
+ };
2350
+ await this.realtimeService.publish(event);
2351
+ }
2352
+ this.logger.debug(`Published ${result.length} data.record.created events`, { object });
2353
+ } else {
2354
+ const event = {
2355
+ type: "data.record.created",
2356
+ object,
2357
+ payload: {
2358
+ recordId: result.id,
2359
+ after: result
2360
+ },
2361
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2362
+ };
2363
+ await this.realtimeService.publish(event);
2364
+ this.logger.debug("Published data.record.created event", { object, recordId: result.id });
2365
+ }
2366
+ } catch (error) {
2367
+ this.logger.warn("Failed to publish data event", { object, error });
2368
+ }
2369
+ }
2173
2370
  return hookContext.result;
2174
2371
  } catch (e) {
2175
2372
  this.logger.error("Insert operation failed", e, { object });
@@ -2216,6 +2413,26 @@ var _ObjectQL = class _ObjectQL {
2216
2413
  hookContext.event = "afterUpdate";
2217
2414
  hookContext.result = result;
2218
2415
  await this.triggerHooks("afterUpdate", hookContext);
2416
+ if (this.realtimeService) {
2417
+ try {
2418
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2419
+ const recordId = String(hookContext.input.id || resultId || "");
2420
+ const event = {
2421
+ type: "data.record.updated",
2422
+ object,
2423
+ payload: {
2424
+ recordId,
2425
+ changes: hookContext.input.data,
2426
+ after: result
2427
+ },
2428
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2429
+ };
2430
+ await this.realtimeService.publish(event);
2431
+ this.logger.debug("Published data.record.updated event", { object, recordId });
2432
+ } catch (error) {
2433
+ this.logger.warn("Failed to publish data event", { object, error });
2434
+ }
2435
+ }
2219
2436
  return hookContext.result;
2220
2437
  } catch (e) {
2221
2438
  this.logger.error("Update operation failed", e, { object });
@@ -2261,6 +2478,24 @@ var _ObjectQL = class _ObjectQL {
2261
2478
  hookContext.event = "afterDelete";
2262
2479
  hookContext.result = result;
2263
2480
  await this.triggerHooks("afterDelete", hookContext);
2481
+ if (this.realtimeService) {
2482
+ try {
2483
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2484
+ const recordId = String(hookContext.input.id || resultId || "");
2485
+ const event = {
2486
+ type: "data.record.deleted",
2487
+ object,
2488
+ payload: {
2489
+ recordId
2490
+ },
2491
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2492
+ };
2493
+ await this.realtimeService.publish(event);
2494
+ this.logger.debug("Published data.record.deleted event", { object, recordId });
2495
+ } catch (error) {
2496
+ this.logger.warn("Failed to publish data event", { object, error });
2497
+ }
2498
+ }
2264
2499
  return hookContext.result;
2265
2500
  } catch (e) {
2266
2501
  this.logger.error("Delete operation failed", e, { object });
@@ -2718,6 +2953,9 @@ var MetadataFacade = class {
2718
2953
  };
2719
2954
 
2720
2955
  // src/plugin.ts
2956
+ function hasLoadMetaFromDb(service) {
2957
+ return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
2958
+ }
2721
2959
  var ObjectQLPlugin = class {
2722
2960
  constructor(ql, hostContext) {
2723
2961
  this.name = "com.objectstack.engine.objectql";
@@ -2774,9 +3012,22 @@ var ObjectQLPlugin = class {
2774
3012
  ctx.logger.debug("Discovered and registered app service (legacy)", { serviceName: name });
2775
3013
  }
2776
3014
  }
3015
+ try {
3016
+ const realtimeService = ctx.getService("realtime");
3017
+ if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
3018
+ ctx.logger.info("[ObjectQLPlugin] Bridging realtime service to ObjectQL for event publishing");
3019
+ this.ql.setRealtimeService(realtimeService);
3020
+ }
3021
+ } catch (e) {
3022
+ ctx.logger.debug("[ObjectQLPlugin] No realtime service found \u2014 data events will not be published", {
3023
+ error: e.message
3024
+ });
3025
+ }
2777
3026
  }
2778
3027
  await this.ql?.init();
3028
+ await this.restoreMetadataFromDb(ctx);
2779
3029
  await this.syncRegisteredSchemas(ctx);
3030
+ await this.bridgeObjectsToMetadataService(ctx);
2780
3031
  this.registerAuditHooks(ctx);
2781
3032
  this.registerTenantMiddleware(ctx);
2782
3033
  ctx.logger.info("ObjectQL engine started", {
@@ -2969,6 +3220,97 @@ var ObjectQLPlugin = class {
2969
3220
  ctx.logger.info("Schema sync complete", { synced, skipped, total: allObjects.length });
2970
3221
  }
2971
3222
  }
3223
+ /**
3224
+ * Restore persisted metadata from the database (sys_metadata) on startup.
3225
+ *
3226
+ * Calls `protocol.loadMetaFromDb()` to bulk-load all active metadata
3227
+ * records (objects, views, apps, etc.) into the in-memory SchemaRegistry.
3228
+ * This closes the persistence loop so that user-created schemas survive
3229
+ * kernel cold starts and redeployments.
3230
+ *
3231
+ * Gracefully degrades when:
3232
+ * - The protocol service is unavailable (e.g., in-memory-only mode).
3233
+ * - `loadMetaFromDb` is not implemented by the protocol shim.
3234
+ * - The underlying driver/table does not exist yet (first-run scenario).
3235
+ */
3236
+ async restoreMetadataFromDb(ctx) {
3237
+ let protocol;
3238
+ try {
3239
+ const service = ctx.getService("protocol");
3240
+ if (!service || !hasLoadMetaFromDb(service)) {
3241
+ ctx.logger.debug("Protocol service does not support loadMetaFromDb, skipping DB restore");
3242
+ return;
3243
+ }
3244
+ protocol = service;
3245
+ } catch (e) {
3246
+ ctx.logger.debug("Protocol service unavailable, skipping DB restore", {
3247
+ error: e instanceof Error ? e.message : String(e)
3248
+ });
3249
+ return;
3250
+ }
3251
+ try {
3252
+ const { loaded, errors } = await protocol.loadMetaFromDb();
3253
+ if (loaded > 0 || errors > 0) {
3254
+ ctx.logger.info("Metadata restored from database to SchemaRegistry", { loaded, errors });
3255
+ } else {
3256
+ ctx.logger.debug("No persisted metadata found in database");
3257
+ }
3258
+ } catch (e) {
3259
+ ctx.logger.debug("DB metadata restore failed (non-fatal)", {
3260
+ error: e instanceof Error ? e.message : String(e)
3261
+ });
3262
+ }
3263
+ }
3264
+ /**
3265
+ * Bridge all SchemaRegistry objects to the metadata service.
3266
+ *
3267
+ * This ensures objects registered by plugins and loaded from sys_metadata
3268
+ * are visible to AI tools and other consumers that query IMetadataService.
3269
+ *
3270
+ * Runs after both restoreMetadataFromDb() and syncRegisteredSchemas() to
3271
+ * catch all objects in the SchemaRegistry regardless of their source.
3272
+ */
3273
+ async bridgeObjectsToMetadataService(ctx) {
3274
+ try {
3275
+ const metadataService = ctx.getService("metadata");
3276
+ if (!metadataService || typeof metadataService.register !== "function") {
3277
+ ctx.logger.debug("Metadata service unavailable for bridging, skipping");
3278
+ return;
3279
+ }
3280
+ if (!this.ql?.registry) {
3281
+ ctx.logger.debug("SchemaRegistry unavailable for bridging, skipping");
3282
+ return;
3283
+ }
3284
+ const objects = this.ql.registry.getAllObjects();
3285
+ let bridged = 0;
3286
+ for (const obj of objects) {
3287
+ try {
3288
+ const existing = await metadataService.getObject(obj.name);
3289
+ if (!existing) {
3290
+ await metadataService.register("object", obj.name, obj);
3291
+ bridged++;
3292
+ }
3293
+ } catch (e) {
3294
+ ctx.logger.debug("Failed to bridge object to metadata service", {
3295
+ object: obj.name,
3296
+ error: e instanceof Error ? e.message : String(e)
3297
+ });
3298
+ }
3299
+ }
3300
+ if (bridged > 0) {
3301
+ ctx.logger.info("Bridged objects from SchemaRegistry to metadata service", {
3302
+ count: bridged,
3303
+ total: objects.length
3304
+ });
3305
+ } else {
3306
+ ctx.logger.debug("No objects needed bridging (all already in metadata service)");
3307
+ }
3308
+ } catch (e) {
3309
+ ctx.logger.debug("Failed to bridge objects to metadata service", {
3310
+ error: e instanceof Error ? e.message : String(e)
3311
+ });
3312
+ }
3313
+ }
2972
3314
  /**
2973
3315
  * Load metadata from external metadata service into ObjectQL registry
2974
3316
  * This enables ObjectQL to use file-based or remote metadata