@objectstack/objectql 3.0.11 → 3.1.0
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 +9 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +119 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +119 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/engine.test.ts +386 -0
- package/src/engine.ts +136 -2
- package/src/protocol.ts +32 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/objectql@3.0
|
|
2
|
+
> @objectstack/objectql@3.1.0 build /home/runner/work/spec/spec/packages/objectql
|
|
3
3
|
> tsup --config ../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
14
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
15
|
-
[32mCJS[39m ⚡️ Build success in
|
|
16
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
17
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m95.52 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m193.28 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 187ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m93.78 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m191.98 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 191ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 23689ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m74.26 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m74.26 KB[39m
|
package/CHANGELOG.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1329,6 +1329,8 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
1329
1329
|
getData(request: {
|
|
1330
1330
|
object: string;
|
|
1331
1331
|
id: string;
|
|
1332
|
+
expand?: string | string[];
|
|
1333
|
+
select?: string | string[];
|
|
1332
1334
|
}): Promise<{
|
|
1333
1335
|
object: string;
|
|
1334
1336
|
id: string;
|
|
@@ -1569,6 +1571,21 @@ declare class ObjectQL implements IDataEngine {
|
|
|
1569
1571
|
*/
|
|
1570
1572
|
init(): Promise<void>;
|
|
1571
1573
|
destroy(): Promise<void>;
|
|
1574
|
+
/** Maximum depth for recursive expand to prevent infinite loops */
|
|
1575
|
+
private static readonly MAX_EXPAND_DEPTH;
|
|
1576
|
+
/**
|
|
1577
|
+
* Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
|
|
1578
|
+
*
|
|
1579
|
+
* This is a driver-agnostic implementation that uses secondary queries ($in batches)
|
|
1580
|
+
* to load related records, then injects them into the result set.
|
|
1581
|
+
*
|
|
1582
|
+
* @param objectName - The source object name
|
|
1583
|
+
* @param records - The records returned by the driver
|
|
1584
|
+
* @param expand - The expand map from QueryAST (field name → nested QueryAST)
|
|
1585
|
+
* @param depth - Current recursion depth (0-based)
|
|
1586
|
+
* @returns Records with expanded lookup fields (IDs replaced by full objects)
|
|
1587
|
+
*/
|
|
1588
|
+
private expandRelatedRecords;
|
|
1572
1589
|
private toQueryAST;
|
|
1573
1590
|
find(object: string, query?: DataEngineQueryOptions): Promise<any[]>;
|
|
1574
1591
|
findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1329,6 +1329,8 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
1329
1329
|
getData(request: {
|
|
1330
1330
|
object: string;
|
|
1331
1331
|
id: string;
|
|
1332
|
+
expand?: string | string[];
|
|
1333
|
+
select?: string | string[];
|
|
1332
1334
|
}): Promise<{
|
|
1333
1335
|
object: string;
|
|
1334
1336
|
id: string;
|
|
@@ -1569,6 +1571,21 @@ declare class ObjectQL implements IDataEngine {
|
|
|
1569
1571
|
*/
|
|
1570
1572
|
init(): Promise<void>;
|
|
1571
1573
|
destroy(): Promise<void>;
|
|
1574
|
+
/** Maximum depth for recursive expand to prevent infinite loops */
|
|
1575
|
+
private static readonly MAX_EXPAND_DEPTH;
|
|
1576
|
+
/**
|
|
1577
|
+
* Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
|
|
1578
|
+
*
|
|
1579
|
+
* This is a driver-agnostic implementation that uses secondary queries ($in batches)
|
|
1580
|
+
* to load related records, then injects them into the result set.
|
|
1581
|
+
*
|
|
1582
|
+
* @param objectName - The source object name
|
|
1583
|
+
* @param records - The records returned by the driver
|
|
1584
|
+
* @param expand - The expand map from QueryAST (field name → nested QueryAST)
|
|
1585
|
+
* @param depth - Current recursion depth (0-based)
|
|
1586
|
+
* @returns Records with expanded lookup fields (IDs replaced by full objects)
|
|
1587
|
+
*/
|
|
1588
|
+
private expandRelatedRecords;
|
|
1572
1589
|
private toQueryAST;
|
|
1573
1590
|
find(object: string, query?: DataEngineQueryOptions): Promise<any[]>;
|
|
1574
1591
|
findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any>;
|
package/dist/index.js
CHANGED
|
@@ -774,6 +774,16 @@ var ObjectStackProtocolImplementation = class {
|
|
|
774
774
|
if (typeof options.populate === "string") {
|
|
775
775
|
options.populate = options.populate.split(",").map((s) => s.trim()).filter(Boolean);
|
|
776
776
|
}
|
|
777
|
+
const expandValue = options.$expand ?? options.expand;
|
|
778
|
+
if (expandValue && !options.populate) {
|
|
779
|
+
if (typeof expandValue === "string") {
|
|
780
|
+
options.populate = expandValue.split(",").map((s) => s.trim()).filter(Boolean);
|
|
781
|
+
} else if (Array.isArray(expandValue)) {
|
|
782
|
+
options.populate = expandValue;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
delete options.$expand;
|
|
786
|
+
delete options.expand;
|
|
777
787
|
for (const key of ["distinct", "count"]) {
|
|
778
788
|
if (options[key] === "true") options[key] = true;
|
|
779
789
|
else if (options[key] === "false") options[key] = false;
|
|
@@ -790,9 +800,16 @@ var ObjectStackProtocolImplementation = class {
|
|
|
790
800
|
};
|
|
791
801
|
}
|
|
792
802
|
async getData(request) {
|
|
793
|
-
const
|
|
803
|
+
const queryOptions = {
|
|
794
804
|
filter: { _id: request.id }
|
|
795
|
-
}
|
|
805
|
+
};
|
|
806
|
+
if (request.select) {
|
|
807
|
+
queryOptions.select = typeof request.select === "string" ? request.select.split(",").map((s) => s.trim()).filter(Boolean) : request.select;
|
|
808
|
+
}
|
|
809
|
+
if (request.expand) {
|
|
810
|
+
queryOptions.populate = typeof request.expand === "string" ? request.expand.split(",").map((s) => s.trim()).filter(Boolean) : request.expand;
|
|
811
|
+
}
|
|
812
|
+
const result = await this.engine.findOne(request.object, queryOptions);
|
|
796
813
|
if (result) {
|
|
797
814
|
return {
|
|
798
815
|
object: request.object,
|
|
@@ -1270,7 +1287,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1270
1287
|
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
1271
1288
|
var import_core = require("@objectstack/core");
|
|
1272
1289
|
var import_system = require("@objectstack/spec/system");
|
|
1273
|
-
var
|
|
1290
|
+
var _ObjectQL = class _ObjectQL {
|
|
1274
1291
|
constructor(hostContext = {}) {
|
|
1275
1292
|
this.drivers = /* @__PURE__ */ new Map();
|
|
1276
1293
|
this.defaultDriver = null;
|
|
@@ -1765,6 +1782,89 @@ var ObjectQL = class _ObjectQL {
|
|
|
1765
1782
|
}
|
|
1766
1783
|
this.logger.info("ObjectQL engine destroyed");
|
|
1767
1784
|
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
|
|
1787
|
+
*
|
|
1788
|
+
* This is a driver-agnostic implementation that uses secondary queries ($in batches)
|
|
1789
|
+
* to load related records, then injects them into the result set.
|
|
1790
|
+
*
|
|
1791
|
+
* @param objectName - The source object name
|
|
1792
|
+
* @param records - The records returned by the driver
|
|
1793
|
+
* @param expand - The expand map from QueryAST (field name → nested QueryAST)
|
|
1794
|
+
* @param depth - Current recursion depth (0-based)
|
|
1795
|
+
* @returns Records with expanded lookup fields (IDs replaced by full objects)
|
|
1796
|
+
*/
|
|
1797
|
+
async expandRelatedRecords(objectName, records, expand, depth = 0) {
|
|
1798
|
+
if (!records || records.length === 0) return records;
|
|
1799
|
+
if (depth >= _ObjectQL.MAX_EXPAND_DEPTH) return records;
|
|
1800
|
+
const objectSchema = SchemaRegistry.getObject(objectName);
|
|
1801
|
+
if (!objectSchema || !objectSchema.fields) return records;
|
|
1802
|
+
for (const [fieldName, nestedAST] of Object.entries(expand)) {
|
|
1803
|
+
const fieldDef = objectSchema.fields[fieldName];
|
|
1804
|
+
if (!fieldDef || !fieldDef.reference) continue;
|
|
1805
|
+
if (fieldDef.type !== "lookup" && fieldDef.type !== "master_detail") continue;
|
|
1806
|
+
const referenceObject = fieldDef.reference;
|
|
1807
|
+
const allIds = [];
|
|
1808
|
+
for (const record of records) {
|
|
1809
|
+
const val = record[fieldName];
|
|
1810
|
+
if (val == null) continue;
|
|
1811
|
+
if (Array.isArray(val)) {
|
|
1812
|
+
allIds.push(...val.filter((id) => id != null));
|
|
1813
|
+
} else if (typeof val === "object") {
|
|
1814
|
+
continue;
|
|
1815
|
+
} else {
|
|
1816
|
+
allIds.push(val);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const uniqueIds = [...new Set(allIds)];
|
|
1820
|
+
if (uniqueIds.length === 0) continue;
|
|
1821
|
+
try {
|
|
1822
|
+
const relatedQuery = {
|
|
1823
|
+
object: referenceObject,
|
|
1824
|
+
where: { _id: { $in: uniqueIds } },
|
|
1825
|
+
...nestedAST.fields ? { fields: nestedAST.fields } : {},
|
|
1826
|
+
...nestedAST.orderBy ? { orderBy: nestedAST.orderBy } : {}
|
|
1827
|
+
};
|
|
1828
|
+
const driver = this.getDriver(referenceObject);
|
|
1829
|
+
const relatedRecords = await driver.find(referenceObject, relatedQuery) ?? [];
|
|
1830
|
+
const recordMap = /* @__PURE__ */ new Map();
|
|
1831
|
+
for (const rec of relatedRecords) {
|
|
1832
|
+
const id = rec._id ?? rec.id;
|
|
1833
|
+
if (id != null) recordMap.set(String(id), rec);
|
|
1834
|
+
}
|
|
1835
|
+
if (nestedAST.expand && Object.keys(nestedAST.expand).length > 0) {
|
|
1836
|
+
const expandedRelated = await this.expandRelatedRecords(
|
|
1837
|
+
referenceObject,
|
|
1838
|
+
relatedRecords,
|
|
1839
|
+
nestedAST.expand,
|
|
1840
|
+
depth + 1
|
|
1841
|
+
);
|
|
1842
|
+
recordMap.clear();
|
|
1843
|
+
for (const rec of expandedRelated) {
|
|
1844
|
+
const id = rec._id ?? rec.id;
|
|
1845
|
+
if (id != null) recordMap.set(String(id), rec);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
for (const record of records) {
|
|
1849
|
+
const val = record[fieldName];
|
|
1850
|
+
if (val == null) continue;
|
|
1851
|
+
if (Array.isArray(val)) {
|
|
1852
|
+
record[fieldName] = val.map((id) => recordMap.get(String(id)) ?? id);
|
|
1853
|
+
} else if (typeof val !== "object") {
|
|
1854
|
+
record[fieldName] = recordMap.get(String(val)) ?? val;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
} catch (e) {
|
|
1858
|
+
this.logger.warn("Failed to expand relationship field; retaining foreign key IDs", {
|
|
1859
|
+
object: objectName,
|
|
1860
|
+
field: fieldName,
|
|
1861
|
+
reference: referenceObject,
|
|
1862
|
+
error: e.message
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return records;
|
|
1867
|
+
}
|
|
1768
1868
|
// ============================================
|
|
1769
1869
|
// Helper: Query Conversion
|
|
1770
1870
|
// ============================================
|
|
@@ -1824,7 +1924,10 @@ var ObjectQL = class _ObjectQL {
|
|
|
1824
1924
|
};
|
|
1825
1925
|
await this.triggerHooks("beforeFind", hookContext);
|
|
1826
1926
|
try {
|
|
1827
|
-
|
|
1927
|
+
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
1928
|
+
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
1929
|
+
result = await this.expandRelatedRecords(object, result, ast.expand, 0);
|
|
1930
|
+
}
|
|
1828
1931
|
hookContext.event = "afterFind";
|
|
1829
1932
|
hookContext.result = result;
|
|
1830
1933
|
await this.triggerHooks("afterFind", hookContext);
|
|
@@ -1850,7 +1953,12 @@ var ObjectQL = class _ObjectQL {
|
|
|
1850
1953
|
context: query?.context
|
|
1851
1954
|
};
|
|
1852
1955
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
1853
|
-
|
|
1956
|
+
let result = await driver.findOne(objectName, opCtx.ast);
|
|
1957
|
+
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
1958
|
+
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0);
|
|
1959
|
+
result = expanded[0];
|
|
1960
|
+
}
|
|
1961
|
+
return result;
|
|
1854
1962
|
});
|
|
1855
1963
|
return opCtx.result;
|
|
1856
1964
|
}
|
|
@@ -2199,6 +2307,12 @@ var ObjectQL = class _ObjectQL {
|
|
|
2199
2307
|
return ql;
|
|
2200
2308
|
}
|
|
2201
2309
|
};
|
|
2310
|
+
// ============================================
|
|
2311
|
+
// Helper: Expand Related Records
|
|
2312
|
+
// ============================================
|
|
2313
|
+
/** Maximum depth for recursive expand to prevent infinite loops */
|
|
2314
|
+
_ObjectQL.MAX_EXPAND_DEPTH = 3;
|
|
2315
|
+
var ObjectQL = _ObjectQL;
|
|
2202
2316
|
var ObjectRepository = class {
|
|
2203
2317
|
constructor(objectName, context, engine) {
|
|
2204
2318
|
this.objectName = objectName;
|