@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.mjs CHANGED
@@ -301,7 +301,7 @@ var SchemaRegistry = class {
301
301
  if (type === "object") {
302
302
  return ObjectSchema.parse(item);
303
303
  }
304
- if (type === "apps") {
304
+ if (type === "app") {
305
305
  return AppSchema.parse(item);
306
306
  }
307
307
  if (type === "package") {
@@ -451,13 +451,13 @@ var SchemaRegistry = class {
451
451
  // App Helpers
452
452
  // ==========================================
453
453
  static registerApp(app, packageId) {
454
- this.registerItem("apps", app, "name", packageId);
454
+ this.registerItem("app", app, "name", packageId);
455
455
  }
456
456
  static getApp(name) {
457
- return this.getItem("apps", name);
457
+ return this.getItem("app", name);
458
458
  }
459
459
  static getAllApps() {
460
- return this.listItems("apps");
460
+ return this.listItems("app");
461
461
  }
462
462
  // ==========================================
463
463
  // Plugin Helpers
@@ -513,6 +513,7 @@ SchemaRegistry.metadata = /* @__PURE__ */ new Map();
513
513
 
514
514
  // src/protocol.ts
515
515
  import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
516
+ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared";
516
517
  function simpleHash(str) {
517
518
  let hash = 0;
518
519
  for (let i = 0; i < str.length; i++) {
@@ -640,16 +641,25 @@ var ObjectStackProtocolImplementation = class {
640
641
  };
641
642
  }
642
643
  async getMetaTypes() {
643
- return {
644
- types: SchemaRegistry.getRegisteredTypes()
645
- };
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 };
646
656
  }
647
657
  async getMetaItems(request) {
648
658
  const { packageId } = request;
649
659
  let items = SchemaRegistry.listItems(request.type, packageId);
650
660
  if (items.length === 0) {
651
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
652
- items = SchemaRegistry.listItems(alt, packageId);
661
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
662
+ if (alt) items = SchemaRegistry.listItems(alt, packageId);
653
663
  }
654
664
  if (items.length === 0) {
655
665
  try {
@@ -665,21 +675,50 @@ var ObjectStackProtocolImplementation = class {
665
675
  return data;
666
676
  });
667
677
  } else {
668
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
669
- const altRecords = await this.engine.find("sys_metadata", {
670
- where: { type: alt, state: "active" }
671
- });
672
- if (altRecords && altRecords.length > 0) {
673
- items = altRecords.map((record) => {
674
- const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
675
- SchemaRegistry.registerItem(request.type, data, "name");
676
- 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" }
677
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
+ }
678
690
  }
679
691
  }
680
692
  } catch {
681
693
  }
682
694
  }
695
+ try {
696
+ const services = this.getServicesRegistry?.();
697
+ const metadataService = services?.get("metadata");
698
+ if (metadataService && typeof metadataService.list === "function") {
699
+ let runtimeItems = await metadataService.list(request.type);
700
+ if (packageId && runtimeItems && runtimeItems.length > 0) {
701
+ runtimeItems = runtimeItems.filter((item) => item?._packageId === packageId);
702
+ }
703
+ if (runtimeItems && runtimeItems.length > 0) {
704
+ const itemMap = /* @__PURE__ */ new Map();
705
+ for (const item of items) {
706
+ const entry = item;
707
+ if (entry && typeof entry === "object" && "name" in entry) {
708
+ itemMap.set(entry.name, entry);
709
+ }
710
+ }
711
+ for (const item of runtimeItems) {
712
+ const entry = item;
713
+ if (entry && typeof entry === "object" && "name" in entry) {
714
+ itemMap.set(entry.name, entry);
715
+ }
716
+ }
717
+ items = Array.from(itemMap.values());
718
+ }
719
+ }
720
+ } catch {
721
+ }
683
722
  return {
684
723
  type: request.type,
685
724
  items
@@ -688,8 +727,8 @@ var ObjectStackProtocolImplementation = class {
688
727
  async getMetaItem(request) {
689
728
  let item = SchemaRegistry.getItem(request.type, request.name);
690
729
  if (item === void 0) {
691
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
692
- item = SchemaRegistry.getItem(alt, request.name);
730
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
731
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
693
732
  }
694
733
  if (item === void 0) {
695
734
  try {
@@ -700,18 +739,30 @@ var ObjectStackProtocolImplementation = class {
700
739
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
701
740
  SchemaRegistry.registerItem(request.type, item, "name");
702
741
  } else {
703
- const alt = request.type.endsWith("s") ? request.type.slice(0, -1) : request.type + "s";
704
- const altRecord = await this.engine.findOne("sys_metadata", {
705
- where: { type: alt, name: request.name, state: "active" }
706
- });
707
- if (altRecord) {
708
- item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
709
- SchemaRegistry.registerItem(request.type, item, "name");
742
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
743
+ if (alt) {
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");
750
+ }
710
751
  }
711
752
  }
712
753
  } catch {
713
754
  }
714
755
  }
756
+ if (item === void 0) {
757
+ try {
758
+ const services = this.getServicesRegistry?.();
759
+ const metadataService = services?.get("metadata");
760
+ if (metadataService && typeof metadataService.get === "function") {
761
+ item = await metadataService.get(request.type, request.name);
762
+ }
763
+ } catch {
764
+ }
765
+ }
715
766
  return {
716
767
  type: request.type,
717
768
  name: request.name,
@@ -942,7 +993,21 @@ var ObjectStackProtocolImplementation = class {
942
993
  // ==========================================
943
994
  async getMetaItemCached(request) {
944
995
  try {
945
- const item = SchemaRegistry.getItem(request.type, request.name);
996
+ let item = SchemaRegistry.getItem(request.type, request.name);
997
+ if (!item) {
998
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
999
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
1000
+ }
1001
+ if (!item) {
1002
+ try {
1003
+ const services = this.getServicesRegistry?.();
1004
+ const metadataService = services?.get("metadata");
1005
+ if (metadataService && typeof metadataService.get === "function") {
1006
+ item = await metadataService.get(request.type, request.name);
1007
+ }
1008
+ } catch {
1009
+ }
1010
+ }
946
1011
  if (!item) {
947
1012
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
948
1013
  }
@@ -1294,10 +1359,11 @@ var ObjectStackProtocolImplementation = class {
1294
1359
  for (const record of records) {
1295
1360
  try {
1296
1361
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1297
- if (record.type === "object") {
1362
+ const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
1363
+ if (normalizedType === "object") {
1298
1364
  SchemaRegistry.registerObject(data, record.packageId || "sys_metadata");
1299
1365
  } else {
1300
- SchemaRegistry.registerItem(record.type, data, "name");
1366
+ SchemaRegistry.registerItem(normalizedType, data, "name");
1301
1367
  }
1302
1368
  loaded++;
1303
1369
  } catch (e) {
@@ -1447,10 +1513,15 @@ var ObjectStackProtocolImplementation = class {
1447
1513
  import { ExecutionContextSchema } from "@objectstack/spec/kernel";
1448
1514
  import { createLogger } from "@objectstack/core";
1449
1515
  import { CoreServiceName } from "@objectstack/spec/system";
1516
+ import { pluralToSingular } from "@objectstack/spec/shared";
1450
1517
  var _ObjectQL = class _ObjectQL {
1451
1518
  constructor(hostContext = {}) {
1452
1519
  this.drivers = /* @__PURE__ */ new Map();
1453
1520
  this.defaultDriver = null;
1521
+ // Datasource mapping rules (imported from defineStack)
1522
+ this.datasourceMapping = [];
1523
+ // Package manifests registry (for defaultDatasource lookup)
1524
+ this.manifests = /* @__PURE__ */ new Map();
1454
1525
  // Per-object hooks with priority support
1455
1526
  this.hooks = /* @__PURE__ */ new Map([
1456
1527
  ["beforeFind", []],
@@ -1648,6 +1719,9 @@ var _ObjectQL = class _ObjectQL {
1648
1719
  const id = manifest.id || manifest.name;
1649
1720
  const namespace = manifest.namespace;
1650
1721
  this.logger.debug("Registering package manifest", { id, namespace });
1722
+ if (id) {
1723
+ this.manifests.set(id, manifest);
1724
+ }
1651
1725
  SchemaRegistry.installPackage(manifest);
1652
1726
  this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
1653
1727
  if (manifest.objects) {
@@ -1737,7 +1811,7 @@ var _ObjectQL = class _ObjectQL {
1737
1811
  for (const item of items) {
1738
1812
  const itemName = item.name || item.id;
1739
1813
  if (itemName) {
1740
- SchemaRegistry.registerItem(key, item, "name", id);
1814
+ SchemaRegistry.registerItem(pluralToSingular(key), item, "name", id);
1741
1815
  }
1742
1816
  }
1743
1817
  }
@@ -1843,7 +1917,7 @@ var _ObjectQL = class _ObjectQL {
1843
1917
  for (const item of items) {
1844
1918
  const itemName = item.name || item.id;
1845
1919
  if (itemName) {
1846
- SchemaRegistry.registerItem(key, item, "name", ownerId);
1920
+ SchemaRegistry.registerItem(pluralToSingular(key), item, "name", ownerId);
1847
1921
  }
1848
1922
  }
1849
1923
  }
@@ -1867,6 +1941,16 @@ var _ObjectQL = class _ObjectQL {
1867
1941
  this.logger.info("Set default driver", { driverName: driver.name });
1868
1942
  }
1869
1943
  }
1944
+ /**
1945
+ * Set the realtime service for publishing data change events.
1946
+ * Should be called after kernel resolves the realtime service.
1947
+ *
1948
+ * @param service - An IRealtimeService instance for event publishing
1949
+ */
1950
+ setRealtimeService(service) {
1951
+ this.realtimeService = service;
1952
+ this.logger.info("RealtimeService configured for data events");
1953
+ }
1870
1954
  /**
1871
1955
  * Helper to get object definition
1872
1956
  */
@@ -1892,27 +1976,99 @@ var _ObjectQL = class _ObjectQL {
1892
1976
  }
1893
1977
  /**
1894
1978
  * Helper to get the target driver
1979
+ *
1980
+ * Resolution priority (first match wins):
1981
+ * 1. Object's explicit `datasource` field (if not 'default')
1982
+ * 2. DatasourceMapping rules (namespace/package/pattern matching)
1983
+ * 3. Package's `defaultDatasource` from manifest
1984
+ * 4. Global default driver
1895
1985
  */
1896
1986
  getDriver(objectName) {
1897
1987
  const object = SchemaRegistry.getObject(objectName);
1898
- if (object) {
1899
- const datasourceName = object.datasource || "default";
1900
- if (datasourceName === "default") {
1901
- if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
1902
- return this.drivers.get(this.defaultDriver);
1903
- }
1904
- } else {
1905
- if (this.drivers.has(datasourceName)) {
1906
- return this.drivers.get(datasourceName);
1988
+ if (object?.datasource && object.datasource !== "default") {
1989
+ if (this.drivers.has(object.datasource)) {
1990
+ return this.drivers.get(object.datasource);
1991
+ }
1992
+ throw new Error(`[ObjectQL] Datasource '${object.datasource}' configured for object '${objectName}' is not registered.`);
1993
+ }
1994
+ const mappedDatasource = this.resolveDatasourceFromMapping(objectName, object);
1995
+ if (mappedDatasource && this.drivers.has(mappedDatasource)) {
1996
+ this.logger.debug("Resolved datasource from mapping", {
1997
+ object: objectName,
1998
+ datasource: mappedDatasource
1999
+ });
2000
+ return this.drivers.get(mappedDatasource);
2001
+ }
2002
+ const fqn = object?.name || objectName;
2003
+ const owner = SchemaRegistry.getObjectOwner(fqn);
2004
+ if (owner?.packageId) {
2005
+ const manifest = this.manifests.get(owner.packageId);
2006
+ if (manifest?.defaultDatasource && manifest.defaultDatasource !== "default") {
2007
+ if (this.drivers.has(manifest.defaultDatasource)) {
2008
+ this.logger.debug("Resolved datasource from package manifest", {
2009
+ object: objectName,
2010
+ package: owner.packageId,
2011
+ datasource: manifest.defaultDatasource
2012
+ });
2013
+ return this.drivers.get(manifest.defaultDatasource);
1907
2014
  }
1908
- throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
1909
2015
  }
1910
2016
  }
1911
- if (this.defaultDriver) {
2017
+ if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
1912
2018
  return this.drivers.get(this.defaultDriver);
1913
2019
  }
1914
2020
  throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
1915
2021
  }
2022
+ /**
2023
+ * Resolve datasource from mapping rules
2024
+ *
2025
+ * Rules are evaluated in order (or by priority if specified).
2026
+ * First matching rule wins.
2027
+ */
2028
+ resolveDatasourceFromMapping(objectName, object) {
2029
+ if (!this.datasourceMapping || this.datasourceMapping.length === 0) {
2030
+ return null;
2031
+ }
2032
+ const sortedRules = [...this.datasourceMapping].sort((a, b) => {
2033
+ const aPriority = a.priority ?? 1e3;
2034
+ const bPriority = b.priority ?? 1e3;
2035
+ return aPriority - bPriority;
2036
+ });
2037
+ for (const rule of sortedRules) {
2038
+ if (rule.namespace && object?.namespace === rule.namespace) {
2039
+ return rule.datasource;
2040
+ }
2041
+ if (rule.package && object?.packageId === rule.package) {
2042
+ return rule.datasource;
2043
+ }
2044
+ if (rule.objectPattern && this.matchPattern(objectName, rule.objectPattern)) {
2045
+ return rule.datasource;
2046
+ }
2047
+ if (rule.default) {
2048
+ return rule.datasource;
2049
+ }
2050
+ }
2051
+ return null;
2052
+ }
2053
+ /**
2054
+ * Simple glob pattern matching
2055
+ * Supports * (any chars) and ? (single char)
2056
+ */
2057
+ matchPattern(objectName, pattern) {
2058
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2059
+ const regex = new RegExp(`^${regexPattern}$`);
2060
+ return regex.test(objectName);
2061
+ }
2062
+ /**
2063
+ * Set datasource mapping rules
2064
+ * Called by ObjectQLPlugin during bootstrap
2065
+ */
2066
+ setDatasourceMapping(rules) {
2067
+ this.datasourceMapping = rules;
2068
+ this.logger.info("Datasource mapping rules configured", {
2069
+ ruleCount: rules.length
2070
+ });
2071
+ }
1916
2072
  /**
1917
2073
  * Initialize the engine and all registered drivers
1918
2074
  */
@@ -1921,14 +2077,22 @@ var _ObjectQL = class _ObjectQL {
1921
2077
  driverCount: this.drivers.size,
1922
2078
  drivers: Array.from(this.drivers.keys())
1923
2079
  });
2080
+ const failedDrivers = [];
1924
2081
  for (const [name, driver] of this.drivers) {
1925
2082
  try {
1926
2083
  await driver.connect();
1927
2084
  this.logger.info("Driver connected successfully", { driverName: name });
1928
2085
  } catch (e) {
2086
+ failedDrivers.push(name);
1929
2087
  this.logger.error("Failed to connect driver", e, { driverName: name });
1930
2088
  }
1931
2089
  }
2090
+ if (failedDrivers.length > 0) {
2091
+ this.logger.warn(
2092
+ `${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. Operations may recover via lazy reconnection or fail at query time.`,
2093
+ { failedDrivers }
2094
+ );
2095
+ }
1932
2096
  this.logger.info("ObjectQL engine initialization complete");
1933
2097
  }
1934
2098
  async destroy() {
@@ -2130,6 +2294,39 @@ var _ObjectQL = class _ObjectQL {
2130
2294
  hookContext.event = "afterInsert";
2131
2295
  hookContext.result = result;
2132
2296
  await this.triggerHooks("afterInsert", hookContext);
2297
+ if (this.realtimeService) {
2298
+ try {
2299
+ if (Array.isArray(result)) {
2300
+ for (const record of result) {
2301
+ const event = {
2302
+ type: "data.record.created",
2303
+ object,
2304
+ payload: {
2305
+ recordId: record.id,
2306
+ after: record
2307
+ },
2308
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2309
+ };
2310
+ await this.realtimeService.publish(event);
2311
+ }
2312
+ this.logger.debug(`Published ${result.length} data.record.created events`, { object });
2313
+ } else {
2314
+ const event = {
2315
+ type: "data.record.created",
2316
+ object,
2317
+ payload: {
2318
+ recordId: result.id,
2319
+ after: result
2320
+ },
2321
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2322
+ };
2323
+ await this.realtimeService.publish(event);
2324
+ this.logger.debug("Published data.record.created event", { object, recordId: result.id });
2325
+ }
2326
+ } catch (error) {
2327
+ this.logger.warn("Failed to publish data event", { object, error });
2328
+ }
2329
+ }
2133
2330
  return hookContext.result;
2134
2331
  } catch (e) {
2135
2332
  this.logger.error("Insert operation failed", e, { object });
@@ -2176,6 +2373,26 @@ var _ObjectQL = class _ObjectQL {
2176
2373
  hookContext.event = "afterUpdate";
2177
2374
  hookContext.result = result;
2178
2375
  await this.triggerHooks("afterUpdate", hookContext);
2376
+ if (this.realtimeService) {
2377
+ try {
2378
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2379
+ const recordId = String(hookContext.input.id || resultId || "");
2380
+ const event = {
2381
+ type: "data.record.updated",
2382
+ object,
2383
+ payload: {
2384
+ recordId,
2385
+ changes: hookContext.input.data,
2386
+ after: result
2387
+ },
2388
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2389
+ };
2390
+ await this.realtimeService.publish(event);
2391
+ this.logger.debug("Published data.record.updated event", { object, recordId });
2392
+ } catch (error) {
2393
+ this.logger.warn("Failed to publish data event", { object, error });
2394
+ }
2395
+ }
2179
2396
  return hookContext.result;
2180
2397
  } catch (e) {
2181
2398
  this.logger.error("Update operation failed", e, { object });
@@ -2221,6 +2438,24 @@ var _ObjectQL = class _ObjectQL {
2221
2438
  hookContext.event = "afterDelete";
2222
2439
  hookContext.result = result;
2223
2440
  await this.triggerHooks("afterDelete", hookContext);
2441
+ if (this.realtimeService) {
2442
+ try {
2443
+ const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
2444
+ const recordId = String(hookContext.input.id || resultId || "");
2445
+ const event = {
2446
+ type: "data.record.deleted",
2447
+ object,
2448
+ payload: {
2449
+ recordId
2450
+ },
2451
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2452
+ };
2453
+ await this.realtimeService.publish(event);
2454
+ this.logger.debug("Published data.record.deleted event", { object, recordId });
2455
+ } catch (error) {
2456
+ this.logger.warn("Failed to publish data event", { object, error });
2457
+ }
2458
+ }
2224
2459
  return hookContext.result;
2225
2460
  } catch (e) {
2226
2461
  this.logger.error("Delete operation failed", e, { object });
@@ -2678,6 +2913,9 @@ var MetadataFacade = class {
2678
2913
  };
2679
2914
 
2680
2915
  // src/plugin.ts
2916
+ function hasLoadMetaFromDb(service) {
2917
+ return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
2918
+ }
2681
2919
  var ObjectQLPlugin = class {
2682
2920
  constructor(ql, hostContext) {
2683
2921
  this.name = "com.objectstack.engine.objectql";
@@ -2734,9 +2972,22 @@ var ObjectQLPlugin = class {
2734
2972
  ctx.logger.debug("Discovered and registered app service (legacy)", { serviceName: name });
2735
2973
  }
2736
2974
  }
2975
+ try {
2976
+ const realtimeService = ctx.getService("realtime");
2977
+ if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
2978
+ ctx.logger.info("[ObjectQLPlugin] Bridging realtime service to ObjectQL for event publishing");
2979
+ this.ql.setRealtimeService(realtimeService);
2980
+ }
2981
+ } catch (e) {
2982
+ ctx.logger.debug("[ObjectQLPlugin] No realtime service found \u2014 data events will not be published", {
2983
+ error: e.message
2984
+ });
2985
+ }
2737
2986
  }
2738
2987
  await this.ql?.init();
2988
+ await this.restoreMetadataFromDb(ctx);
2739
2989
  await this.syncRegisteredSchemas(ctx);
2990
+ await this.bridgeObjectsToMetadataService(ctx);
2740
2991
  this.registerAuditHooks(ctx);
2741
2992
  this.registerTenantMiddleware(ctx);
2742
2993
  ctx.logger.info("ObjectQL engine started", {
@@ -2929,6 +3180,97 @@ var ObjectQLPlugin = class {
2929
3180
  ctx.logger.info("Schema sync complete", { synced, skipped, total: allObjects.length });
2930
3181
  }
2931
3182
  }
3183
+ /**
3184
+ * Restore persisted metadata from the database (sys_metadata) on startup.
3185
+ *
3186
+ * Calls `protocol.loadMetaFromDb()` to bulk-load all active metadata
3187
+ * records (objects, views, apps, etc.) into the in-memory SchemaRegistry.
3188
+ * This closes the persistence loop so that user-created schemas survive
3189
+ * kernel cold starts and redeployments.
3190
+ *
3191
+ * Gracefully degrades when:
3192
+ * - The protocol service is unavailable (e.g., in-memory-only mode).
3193
+ * - `loadMetaFromDb` is not implemented by the protocol shim.
3194
+ * - The underlying driver/table does not exist yet (first-run scenario).
3195
+ */
3196
+ async restoreMetadataFromDb(ctx) {
3197
+ let protocol;
3198
+ try {
3199
+ const service = ctx.getService("protocol");
3200
+ if (!service || !hasLoadMetaFromDb(service)) {
3201
+ ctx.logger.debug("Protocol service does not support loadMetaFromDb, skipping DB restore");
3202
+ return;
3203
+ }
3204
+ protocol = service;
3205
+ } catch (e) {
3206
+ ctx.logger.debug("Protocol service unavailable, skipping DB restore", {
3207
+ error: e instanceof Error ? e.message : String(e)
3208
+ });
3209
+ return;
3210
+ }
3211
+ try {
3212
+ const { loaded, errors } = await protocol.loadMetaFromDb();
3213
+ if (loaded > 0 || errors > 0) {
3214
+ ctx.logger.info("Metadata restored from database to SchemaRegistry", { loaded, errors });
3215
+ } else {
3216
+ ctx.logger.debug("No persisted metadata found in database");
3217
+ }
3218
+ } catch (e) {
3219
+ ctx.logger.debug("DB metadata restore failed (non-fatal)", {
3220
+ error: e instanceof Error ? e.message : String(e)
3221
+ });
3222
+ }
3223
+ }
3224
+ /**
3225
+ * Bridge all SchemaRegistry objects to the metadata service.
3226
+ *
3227
+ * This ensures objects registered by plugins and loaded from sys_metadata
3228
+ * are visible to AI tools and other consumers that query IMetadataService.
3229
+ *
3230
+ * Runs after both restoreMetadataFromDb() and syncRegisteredSchemas() to
3231
+ * catch all objects in the SchemaRegistry regardless of their source.
3232
+ */
3233
+ async bridgeObjectsToMetadataService(ctx) {
3234
+ try {
3235
+ const metadataService = ctx.getService("metadata");
3236
+ if (!metadataService || typeof metadataService.register !== "function") {
3237
+ ctx.logger.debug("Metadata service unavailable for bridging, skipping");
3238
+ return;
3239
+ }
3240
+ if (!this.ql?.registry) {
3241
+ ctx.logger.debug("SchemaRegistry unavailable for bridging, skipping");
3242
+ return;
3243
+ }
3244
+ const objects = this.ql.registry.getAllObjects();
3245
+ let bridged = 0;
3246
+ for (const obj of objects) {
3247
+ try {
3248
+ const existing = await metadataService.getObject(obj.name);
3249
+ if (!existing) {
3250
+ await metadataService.register("object", obj.name, obj);
3251
+ bridged++;
3252
+ }
3253
+ } catch (e) {
3254
+ ctx.logger.debug("Failed to bridge object to metadata service", {
3255
+ object: obj.name,
3256
+ error: e instanceof Error ? e.message : String(e)
3257
+ });
3258
+ }
3259
+ }
3260
+ if (bridged > 0) {
3261
+ ctx.logger.info("Bridged objects from SchemaRegistry to metadata service", {
3262
+ count: bridged,
3263
+ total: objects.length
3264
+ });
3265
+ } else {
3266
+ ctx.logger.debug("No objects needed bridging (all already in metadata service)");
3267
+ }
3268
+ } catch (e) {
3269
+ ctx.logger.debug("Failed to bridge objects to metadata service", {
3270
+ error: e instanceof Error ? e.message : String(e)
3271
+ });
3272
+ }
3273
+ }
2932
3274
  /**
2933
3275
  * Load metadata from external metadata service into ObjectQL registry
2934
3276
  * This enables ObjectQL to use file-based or remote metadata