@objectstack/objectql 3.0.10 → 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 +18 -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/dist/index.mjs
CHANGED
|
@@ -734,6 +734,16 @@ var ObjectStackProtocolImplementation = class {
|
|
|
734
734
|
if (typeof options.populate === "string") {
|
|
735
735
|
options.populate = options.populate.split(",").map((s) => s.trim()).filter(Boolean);
|
|
736
736
|
}
|
|
737
|
+
const expandValue = options.$expand ?? options.expand;
|
|
738
|
+
if (expandValue && !options.populate) {
|
|
739
|
+
if (typeof expandValue === "string") {
|
|
740
|
+
options.populate = expandValue.split(",").map((s) => s.trim()).filter(Boolean);
|
|
741
|
+
} else if (Array.isArray(expandValue)) {
|
|
742
|
+
options.populate = expandValue;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
delete options.$expand;
|
|
746
|
+
delete options.expand;
|
|
737
747
|
for (const key of ["distinct", "count"]) {
|
|
738
748
|
if (options[key] === "true") options[key] = true;
|
|
739
749
|
else if (options[key] === "false") options[key] = false;
|
|
@@ -750,9 +760,16 @@ var ObjectStackProtocolImplementation = class {
|
|
|
750
760
|
};
|
|
751
761
|
}
|
|
752
762
|
async getData(request) {
|
|
753
|
-
const
|
|
763
|
+
const queryOptions = {
|
|
754
764
|
filter: { _id: request.id }
|
|
755
|
-
}
|
|
765
|
+
};
|
|
766
|
+
if (request.select) {
|
|
767
|
+
queryOptions.select = typeof request.select === "string" ? request.select.split(",").map((s) => s.trim()).filter(Boolean) : request.select;
|
|
768
|
+
}
|
|
769
|
+
if (request.expand) {
|
|
770
|
+
queryOptions.populate = typeof request.expand === "string" ? request.expand.split(",").map((s) => s.trim()).filter(Boolean) : request.expand;
|
|
771
|
+
}
|
|
772
|
+
const result = await this.engine.findOne(request.object, queryOptions);
|
|
756
773
|
if (result) {
|
|
757
774
|
return {
|
|
758
775
|
object: request.object,
|
|
@@ -1230,7 +1247,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1230
1247
|
import { ExecutionContextSchema } from "@objectstack/spec/kernel";
|
|
1231
1248
|
import { createLogger } from "@objectstack/core";
|
|
1232
1249
|
import { CoreServiceName } from "@objectstack/spec/system";
|
|
1233
|
-
var
|
|
1250
|
+
var _ObjectQL = class _ObjectQL {
|
|
1234
1251
|
constructor(hostContext = {}) {
|
|
1235
1252
|
this.drivers = /* @__PURE__ */ new Map();
|
|
1236
1253
|
this.defaultDriver = null;
|
|
@@ -1725,6 +1742,89 @@ var ObjectQL = class _ObjectQL {
|
|
|
1725
1742
|
}
|
|
1726
1743
|
this.logger.info("ObjectQL engine destroyed");
|
|
1727
1744
|
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
|
|
1747
|
+
*
|
|
1748
|
+
* This is a driver-agnostic implementation that uses secondary queries ($in batches)
|
|
1749
|
+
* to load related records, then injects them into the result set.
|
|
1750
|
+
*
|
|
1751
|
+
* @param objectName - The source object name
|
|
1752
|
+
* @param records - The records returned by the driver
|
|
1753
|
+
* @param expand - The expand map from QueryAST (field name → nested QueryAST)
|
|
1754
|
+
* @param depth - Current recursion depth (0-based)
|
|
1755
|
+
* @returns Records with expanded lookup fields (IDs replaced by full objects)
|
|
1756
|
+
*/
|
|
1757
|
+
async expandRelatedRecords(objectName, records, expand, depth = 0) {
|
|
1758
|
+
if (!records || records.length === 0) return records;
|
|
1759
|
+
if (depth >= _ObjectQL.MAX_EXPAND_DEPTH) return records;
|
|
1760
|
+
const objectSchema = SchemaRegistry.getObject(objectName);
|
|
1761
|
+
if (!objectSchema || !objectSchema.fields) return records;
|
|
1762
|
+
for (const [fieldName, nestedAST] of Object.entries(expand)) {
|
|
1763
|
+
const fieldDef = objectSchema.fields[fieldName];
|
|
1764
|
+
if (!fieldDef || !fieldDef.reference) continue;
|
|
1765
|
+
if (fieldDef.type !== "lookup" && fieldDef.type !== "master_detail") continue;
|
|
1766
|
+
const referenceObject = fieldDef.reference;
|
|
1767
|
+
const allIds = [];
|
|
1768
|
+
for (const record of records) {
|
|
1769
|
+
const val = record[fieldName];
|
|
1770
|
+
if (val == null) continue;
|
|
1771
|
+
if (Array.isArray(val)) {
|
|
1772
|
+
allIds.push(...val.filter((id) => id != null));
|
|
1773
|
+
} else if (typeof val === "object") {
|
|
1774
|
+
continue;
|
|
1775
|
+
} else {
|
|
1776
|
+
allIds.push(val);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
const uniqueIds = [...new Set(allIds)];
|
|
1780
|
+
if (uniqueIds.length === 0) continue;
|
|
1781
|
+
try {
|
|
1782
|
+
const relatedQuery = {
|
|
1783
|
+
object: referenceObject,
|
|
1784
|
+
where: { _id: { $in: uniqueIds } },
|
|
1785
|
+
...nestedAST.fields ? { fields: nestedAST.fields } : {},
|
|
1786
|
+
...nestedAST.orderBy ? { orderBy: nestedAST.orderBy } : {}
|
|
1787
|
+
};
|
|
1788
|
+
const driver = this.getDriver(referenceObject);
|
|
1789
|
+
const relatedRecords = await driver.find(referenceObject, relatedQuery) ?? [];
|
|
1790
|
+
const recordMap = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const rec of relatedRecords) {
|
|
1792
|
+
const id = rec._id ?? rec.id;
|
|
1793
|
+
if (id != null) recordMap.set(String(id), rec);
|
|
1794
|
+
}
|
|
1795
|
+
if (nestedAST.expand && Object.keys(nestedAST.expand).length > 0) {
|
|
1796
|
+
const expandedRelated = await this.expandRelatedRecords(
|
|
1797
|
+
referenceObject,
|
|
1798
|
+
relatedRecords,
|
|
1799
|
+
nestedAST.expand,
|
|
1800
|
+
depth + 1
|
|
1801
|
+
);
|
|
1802
|
+
recordMap.clear();
|
|
1803
|
+
for (const rec of expandedRelated) {
|
|
1804
|
+
const id = rec._id ?? rec.id;
|
|
1805
|
+
if (id != null) recordMap.set(String(id), rec);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
for (const record of records) {
|
|
1809
|
+
const val = record[fieldName];
|
|
1810
|
+
if (val == null) continue;
|
|
1811
|
+
if (Array.isArray(val)) {
|
|
1812
|
+
record[fieldName] = val.map((id) => recordMap.get(String(id)) ?? id);
|
|
1813
|
+
} else if (typeof val !== "object") {
|
|
1814
|
+
record[fieldName] = recordMap.get(String(val)) ?? val;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
} catch (e) {
|
|
1818
|
+
this.logger.warn("Failed to expand relationship field; retaining foreign key IDs", {
|
|
1819
|
+
object: objectName,
|
|
1820
|
+
field: fieldName,
|
|
1821
|
+
reference: referenceObject,
|
|
1822
|
+
error: e.message
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return records;
|
|
1827
|
+
}
|
|
1728
1828
|
// ============================================
|
|
1729
1829
|
// Helper: Query Conversion
|
|
1730
1830
|
// ============================================
|
|
@@ -1784,7 +1884,10 @@ var ObjectQL = class _ObjectQL {
|
|
|
1784
1884
|
};
|
|
1785
1885
|
await this.triggerHooks("beforeFind", hookContext);
|
|
1786
1886
|
try {
|
|
1787
|
-
|
|
1887
|
+
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
1888
|
+
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
1889
|
+
result = await this.expandRelatedRecords(object, result, ast.expand, 0);
|
|
1890
|
+
}
|
|
1788
1891
|
hookContext.event = "afterFind";
|
|
1789
1892
|
hookContext.result = result;
|
|
1790
1893
|
await this.triggerHooks("afterFind", hookContext);
|
|
@@ -1810,7 +1913,12 @@ var ObjectQL = class _ObjectQL {
|
|
|
1810
1913
|
context: query?.context
|
|
1811
1914
|
};
|
|
1812
1915
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
1813
|
-
|
|
1916
|
+
let result = await driver.findOne(objectName, opCtx.ast);
|
|
1917
|
+
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
1918
|
+
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0);
|
|
1919
|
+
result = expanded[0];
|
|
1920
|
+
}
|
|
1921
|
+
return result;
|
|
1814
1922
|
});
|
|
1815
1923
|
return opCtx.result;
|
|
1816
1924
|
}
|
|
@@ -2159,6 +2267,12 @@ var ObjectQL = class _ObjectQL {
|
|
|
2159
2267
|
return ql;
|
|
2160
2268
|
}
|
|
2161
2269
|
};
|
|
2270
|
+
// ============================================
|
|
2271
|
+
// Helper: Expand Related Records
|
|
2272
|
+
// ============================================
|
|
2273
|
+
/** Maximum depth for recursive expand to prevent infinite loops */
|
|
2274
|
+
_ObjectQL.MAX_EXPAND_DEPTH = 3;
|
|
2275
|
+
var ObjectQL = _ObjectQL;
|
|
2162
2276
|
var ObjectRepository = class {
|
|
2163
2277
|
constructor(objectName, context, engine) {
|
|
2164
2278
|
this.objectName = objectName;
|