@objectstack/objectql 3.0.11 → 3.1.1

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
@@ -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 result = await this.engine.findOne(request.object, {
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 ObjectQL = class _ObjectQL {
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
- const result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
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
- return driver.findOne(objectName, opCtx.ast);
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;