@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +23 -0
- package/dist/index.d.mts +72 -6
- package/dist/index.d.ts +72 -6
- package/dist/index.js +385 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +385 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/datasource-mapping.test.ts +181 -0
- package/src/engine.test.ts +15 -1
- package/src/engine.ts +253 -27
- package/src/plugin.integration.test.ts +170 -0
- package/src/plugin.ts +149 -1
- package/src/protocol.ts +107 -19
- package/src/registry.test.ts +11 -11
- package/src/registry.ts +4 -4
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 === "
|
|
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("
|
|
454
|
+
this.registerItem("app", app, "name", packageId);
|
|
455
455
|
}
|
|
456
456
|
static getApp(name) {
|
|
457
|
-
return this.getItem("
|
|
457
|
+
return this.getItem("app", name);
|
|
458
458
|
}
|
|
459
459
|
static getAllApps() {
|
|
460
|
-
return this.listItems("
|
|
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
|
-
|
|
644
|
-
|
|
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
|
|
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
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
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
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|