@objectstack/objectql 4.0.2 → 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
@@ -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,47 @@ 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
+ 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
+ }
683
719
  return {
684
720
  type: request.type,
685
721
  items
@@ -688,8 +724,8 @@ var ObjectStackProtocolImplementation = class {
688
724
  async getMetaItem(request) {
689
725
  let item = SchemaRegistry.getItem(request.type, request.name);
690
726
  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);
727
+ const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
728
+ if (alt) item = SchemaRegistry.getItem(alt, request.name);
693
729
  }
694
730
  if (item === void 0) {
695
731
  try {
@@ -700,18 +736,30 @@ var ObjectStackProtocolImplementation = class {
700
736
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
701
737
  SchemaRegistry.registerItem(request.type, item, "name");
702
738
  } 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");
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
+ }
710
748
  }
711
749
  }
712
750
  } catch {
713
751
  }
714
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
+ }
715
763
  return {
716
764
  type: request.type,
717
765
  name: request.name,
@@ -942,7 +990,21 @@ var ObjectStackProtocolImplementation = class {
942
990
  // ==========================================
943
991
  async getMetaItemCached(request) {
944
992
  try {
945
- 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
+ }
946
1008
  if (!item) {
947
1009
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
948
1010
  }
@@ -1294,10 +1356,11 @@ var ObjectStackProtocolImplementation = class {
1294
1356
  for (const record of records) {
1295
1357
  try {
1296
1358
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
1297
- if (record.type === "object") {
1359
+ const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
1360
+ if (normalizedType === "object") {
1298
1361
  SchemaRegistry.registerObject(data, record.packageId || "sys_metadata");
1299
1362
  } else {
1300
- SchemaRegistry.registerItem(record.type, data, "name");
1363
+ SchemaRegistry.registerItem(normalizedType, data, "name");
1301
1364
  }
1302
1365
  loaded++;
1303
1366
  } catch (e) {
@@ -1447,6 +1510,7 @@ var ObjectStackProtocolImplementation = class {
1447
1510
  import { ExecutionContextSchema } from "@objectstack/spec/kernel";
1448
1511
  import { createLogger } from "@objectstack/core";
1449
1512
  import { CoreServiceName } from "@objectstack/spec/system";
1513
+ import { pluralToSingular } from "@objectstack/spec/shared";
1450
1514
  var _ObjectQL = class _ObjectQL {
1451
1515
  constructor(hostContext = {}) {
1452
1516
  this.drivers = /* @__PURE__ */ new Map();
@@ -1737,7 +1801,7 @@ var _ObjectQL = class _ObjectQL {
1737
1801
  for (const item of items) {
1738
1802
  const itemName = item.name || item.id;
1739
1803
  if (itemName) {
1740
- SchemaRegistry.registerItem(key, item, "name", id);
1804
+ SchemaRegistry.registerItem(pluralToSingular(key), item, "name", id);
1741
1805
  }
1742
1806
  }
1743
1807
  }
@@ -1843,7 +1907,7 @@ var _ObjectQL = class _ObjectQL {
1843
1907
  for (const item of items) {
1844
1908
  const itemName = item.name || item.id;
1845
1909
  if (itemName) {
1846
- SchemaRegistry.registerItem(key, item, "name", ownerId);
1910
+ SchemaRegistry.registerItem(pluralToSingular(key), item, "name", ownerId);
1847
1911
  }
1848
1912
  }
1849
1913
  }
@@ -1867,6 +1931,16 @@ var _ObjectQL = class _ObjectQL {
1867
1931
  this.logger.info("Set default driver", { driverName: driver.name });
1868
1932
  }
1869
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
+ }
1870
1944
  /**
1871
1945
  * Helper to get object definition
1872
1946
  */
@@ -1921,14 +1995,22 @@ var _ObjectQL = class _ObjectQL {
1921
1995
  driverCount: this.drivers.size,
1922
1996
  drivers: Array.from(this.drivers.keys())
1923
1997
  });
1998
+ const failedDrivers = [];
1924
1999
  for (const [name, driver] of this.drivers) {
1925
2000
  try {
1926
2001
  await driver.connect();
1927
2002
  this.logger.info("Driver connected successfully", { driverName: name });
1928
2003
  } catch (e) {
2004
+ failedDrivers.push(name);
1929
2005
  this.logger.error("Failed to connect driver", e, { driverName: name });
1930
2006
  }
1931
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
+ }
1932
2014
  this.logger.info("ObjectQL engine initialization complete");
1933
2015
  }
1934
2016
  async destroy() {
@@ -2130,6 +2212,39 @@ var _ObjectQL = class _ObjectQL {
2130
2212
  hookContext.event = "afterInsert";
2131
2213
  hookContext.result = result;
2132
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
+ }
2133
2248
  return hookContext.result;
2134
2249
  } catch (e) {
2135
2250
  this.logger.error("Insert operation failed", e, { object });
@@ -2176,6 +2291,26 @@ var _ObjectQL = class _ObjectQL {
2176
2291
  hookContext.event = "afterUpdate";
2177
2292
  hookContext.result = result;
2178
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
+ }
2179
2314
  return hookContext.result;
2180
2315
  } catch (e) {
2181
2316
  this.logger.error("Update operation failed", e, { object });
@@ -2221,6 +2356,24 @@ var _ObjectQL = class _ObjectQL {
2221
2356
  hookContext.event = "afterDelete";
2222
2357
  hookContext.result = result;
2223
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
+ }
2224
2377
  return hookContext.result;
2225
2378
  } catch (e) {
2226
2379
  this.logger.error("Delete operation failed", e, { object });
@@ -2678,6 +2831,9 @@ var MetadataFacade = class {
2678
2831
  };
2679
2832
 
2680
2833
  // src/plugin.ts
2834
+ function hasLoadMetaFromDb(service) {
2835
+ return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
2836
+ }
2681
2837
  var ObjectQLPlugin = class {
2682
2838
  constructor(ql, hostContext) {
2683
2839
  this.name = "com.objectstack.engine.objectql";
@@ -2734,9 +2890,22 @@ var ObjectQLPlugin = class {
2734
2890
  ctx.logger.debug("Discovered and registered app service (legacy)", { serviceName: name });
2735
2891
  }
2736
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);
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
+ });
2903
+ }
2737
2904
  }
2738
2905
  await this.ql?.init();
2906
+ await this.restoreMetadataFromDb(ctx);
2739
2907
  await this.syncRegisteredSchemas(ctx);
2908
+ await this.bridgeObjectsToMetadataService(ctx);
2740
2909
  this.registerAuditHooks(ctx);
2741
2910
  this.registerTenantMiddleware(ctx);
2742
2911
  ctx.logger.info("ObjectQL engine started", {
@@ -2929,6 +3098,97 @@ var ObjectQLPlugin = class {
2929
3098
  ctx.logger.info("Schema sync complete", { synced, skipped, total: allObjects.length });
2930
3099
  }
2931
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
+ }
2932
3192
  /**
2933
3193
  * Load metadata from external metadata service into ObjectQL registry
2934
3194
  * This enables ObjectQL to use file-based or remote metadata