@objectstack/objectql 7.9.0 → 8.0.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/dist/index.js CHANGED
@@ -5103,6 +5103,7 @@ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
5103
5103
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
5104
5104
 
5105
5105
  // src/engine.ts
5106
+ var import_node_async_hooks = require("async_hooks");
5106
5107
  var import_kernel6 = require("@objectstack/spec/kernel");
5107
5108
  var import_core = require("@objectstack/core");
5108
5109
  var import_system2 = require("@objectstack/spec/system");
@@ -5595,7 +5596,7 @@ function optionValues(options) {
5595
5596
  );
5596
5597
  }
5597
5598
  function validateOne(name, def, value) {
5598
- if (def.required && isMissing(value)) {
5599
+ if (def.required && isMissing(value) && def.type !== "autonumber") {
5599
5600
  return { field: name, code: "required", message: `${name} is required` };
5600
5601
  }
5601
5602
  if (isMissing(value)) return null;
@@ -5693,8 +5694,31 @@ var ajv = new import_ajv.default({ allErrors: true, strict: false });
5693
5694
  var jsonSchemaCache = /* @__PURE__ */ new WeakMap();
5694
5695
  function needsPriorRecord(objectSchema) {
5695
5696
  const rules = objectSchema?.validations;
5696
- if (!Array.isArray(rules)) return false;
5697
- return rules.some((r) => ruleNeedsPrior(r));
5697
+ const ruleNeeds = Array.isArray(rules) && rules.some((r) => ruleNeedsPrior(r));
5698
+ return !!(ruleNeeds || fieldsNeedPrior(objectSchema?.fields));
5699
+ }
5700
+ function stripReadonlyWhenFields(objectSchema, data, previous, logger) {
5701
+ const fields = objectSchema?.fields;
5702
+ if (!fields || !data) return data;
5703
+ const merged = { ...previous ?? {}, ...data };
5704
+ let result = data;
5705
+ for (const [name, def] of Object.entries(fields)) {
5706
+ if (!def?.readonlyWhen || !(name in data)) continue;
5707
+ const res = import_formula2.ExpressionEngine.evaluate(toExpression(def.readonlyWhen), {
5708
+ record: merged,
5709
+ previous: previous ?? void 0
5710
+ });
5711
+ if (!res.ok) {
5712
+ logger?.warn?.(`readonlyWhen for '${name}' failed to evaluate \u2014 change allowed through`);
5713
+ continue;
5714
+ }
5715
+ if (res.value === true) {
5716
+ if (result === data) result = { ...data };
5717
+ delete result[name];
5718
+ logger?.warn?.(`Field '${name}' is read-only (readonlyWhen) \u2014 ignoring incoming change`);
5719
+ }
5720
+ }
5721
+ return result;
5698
5722
  }
5699
5723
  function ruleNeedsPrior(r) {
5700
5724
  if (r == null || typeof r !== "object") return false;
@@ -5708,17 +5732,44 @@ function ruleNeedsPrior(r) {
5708
5732
  }
5709
5733
  return false;
5710
5734
  }
5735
+ function isMissing2(v) {
5736
+ return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
5737
+ }
5738
+ function fieldsNeedPrior(fields) {
5739
+ if (!fields) return false;
5740
+ return Object.values(fields).some(
5741
+ (f) => f && (f.requiredWhen || f.conditionalRequired || f.readonlyWhen)
5742
+ );
5743
+ }
5711
5744
  function toExpression(cond) {
5712
5745
  return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
5713
5746
  }
5714
5747
  function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5748
+ if (!data) return;
5715
5749
  const rules = objectSchema?.validations;
5716
- if (!Array.isArray(rules) || rules.length === 0 || !data) return;
5750
+ const hasRules = Array.isArray(rules) && rules.length > 0;
5751
+ const fields = objectSchema?.fields;
5752
+ const hasFieldRules = fieldsNeedPrior(fields);
5753
+ if (!hasRules && !hasFieldRules) return;
5717
5754
  const previous = opts.previous ?? void 0;
5718
5755
  const merged = { ...previous ?? {}, ...data };
5719
5756
  const ctx = { data, merged, previous, mode, logger: opts.logger };
5720
5757
  const errors = [];
5721
- const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5758
+ if (hasFieldRules && fields) {
5759
+ for (const [name, def] of Object.entries(fields)) {
5760
+ const pred = def?.requiredWhen ?? def?.conditionalRequired;
5761
+ if (!pred) continue;
5762
+ const res = import_formula2.ExpressionEngine.evaluate(toExpression(pred), { record: merged, previous });
5763
+ if (!res.ok) {
5764
+ opts.logger?.warn?.(`requiredWhen for '${name}' failed to evaluate \u2014 skipped`);
5765
+ continue;
5766
+ }
5767
+ if (res.value === true && isMissing2(merged[name])) {
5768
+ errors.push({ field: name, code: "required", message: `${name} is required` });
5769
+ }
5770
+ }
5771
+ }
5772
+ const ordered = (hasRules ? rules : []).filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5722
5773
  const events = r.events ?? ["insert", "update"];
5723
5774
  return events.includes(mode);
5724
5775
  }).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
@@ -6096,6 +6147,15 @@ function resolveMetadataItemName(key, item) {
6096
6147
  }
6097
6148
  var _ObjectQL = class _ObjectQL {
6098
6149
  constructor(hostContext = {}) {
6150
+ /**
6151
+ * Ambient transaction store (ADR-0034). While a `transaction()` callback
6152
+ * runs, the active transaction handle lives here so that EVERY data
6153
+ * operation — including internal reads done during a write (reference
6154
+ * checks, hooks, expand) — automatically binds to the same connection
6155
+ * instead of asking the pool for another one and deadlocking on the
6156
+ * single-connection SQLite pool.
6157
+ */
6158
+ this.txStore = new import_node_async_hooks.AsyncLocalStorage();
6099
6159
  this.drivers = /* @__PURE__ */ new Map();
6100
6160
  this.defaultDriver = null;
6101
6161
  // Datasource mapping rules (imported from defineStack)
@@ -6137,6 +6197,12 @@ var _ObjectQL = class _ObjectQL {
6137
6197
  // getDriver()'s owner lookup would route CRUD to the wrong database. Each
6138
6198
  // engine now owns its registry so kernels are fully isolated.
6139
6199
  this._registry = new SchemaRegistry();
6200
+ /** In-memory next-value cache per `object.field` for autonumber generation,
6201
+ * lazily seeded from the current max in the store. */
6202
+ this.autonumberCounters = /* @__PURE__ */ new Map();
6203
+ /** Lazily-built index: child object name → roll-up summary descriptors on
6204
+ * parent objects that aggregate it. Invalidated when packages register. */
6205
+ this.summaryIndex = null;
6140
6206
  this.hostContext = hostContext;
6141
6207
  this.logger = hostContext.logger || (0, import_core.createLogger)({ level: "info", format: "pretty" });
6142
6208
  if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
@@ -6439,13 +6505,14 @@ var _ObjectQL = class _ObjectQL {
6439
6505
  * mask the system path.
6440
6506
  */
6441
6507
  buildDriverOptions(execCtx, base) {
6442
- const hasTx = execCtx?.transaction !== void 0;
6508
+ const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
6509
+ const hasTx = tx !== void 0;
6443
6510
  const hasTenant = execCtx?.tenantId !== void 0;
6444
6511
  const isSystem = execCtx?.isSystem === true;
6445
6512
  if (!hasTx && !hasTenant && !isSystem) return base;
6446
6513
  const opts = base && typeof base === "object" ? { ...base } : {};
6447
6514
  if (hasTx && opts.transaction === void 0) {
6448
- opts.transaction = execCtx.transaction;
6515
+ opts.transaction = tx;
6449
6516
  }
6450
6517
  if (hasTenant && opts.tenantId === void 0) {
6451
6518
  opts.tenantId = execCtx.tenantId;
@@ -6511,6 +6578,66 @@ var _ObjectQL = class _ObjectQL {
6511
6578
  }
6512
6579
  return out;
6513
6580
  }
6581
+ /**
6582
+ * Generate values for empty `autonumber` fields on insert — ONLY for drivers
6583
+ * that do not generate them natively (memory, mongodb). For SQL-backed objects
6584
+ * the driver owns a persistent, atomic `_objectstack_sequences` table and
6585
+ * advertises `supports.autonumber === true`; the engine then defers entirely
6586
+ * and never pre-fills (so the persistent sequence is the single source of
6587
+ * truth — see #1603). Required-validation exempts `autonumber` either way, so
6588
+ * a `required` record number is never rejected for "missing" — the runtime
6589
+ * owns the value, not the client.
6590
+ *
6591
+ * In the fallback path the next value is `max(existing) + 1`, seeded once per
6592
+ * `object.field` from the store then incremented in memory (monotonic within
6593
+ * the process, resilient to deletions). `autonumberFormat` is honored, e.g.
6594
+ * `CASE-{0000}` → `CASE-0042`. NOTE: this in-memory seeding is single-instance.
6595
+ */
6596
+ async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
6597
+ if (driverOwnsAutonumber) return;
6598
+ const fields = this.getSchema(object)?.fields;
6599
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
6600
+ for (const [name, def] of Object.entries(fields)) {
6601
+ if (def?.type !== "autonumber") continue;
6602
+ const current = record[name];
6603
+ if (current != null && current !== "") continue;
6604
+ const key = `${object}.${name}`;
6605
+ let next = this.autonumberCounters.get(key);
6606
+ if (next == null) next = await this.seedAutonumber(object, name, execCtx);
6607
+ next += 1;
6608
+ this.autonumberCounters.set(key, next);
6609
+ const fmt = def.autonumberFormat ?? def.format;
6610
+ record[name] = this.formatAutonumber(fmt, next);
6611
+ }
6612
+ }
6613
+ /** Seed the autonumber counter from the current max numeric value in store. */
6614
+ async seedAutonumber(object, field, execCtx) {
6615
+ try {
6616
+ const rows = await this.find(object, {
6617
+ select: ["id", field],
6618
+ limit: 5e3,
6619
+ context: execCtx
6620
+ });
6621
+ let max = 0;
6622
+ for (const r of rows || []) {
6623
+ const v = r?.[field];
6624
+ if (v == null) continue;
6625
+ const m = String(v).match(/(\d+)(?!.*\d)/);
6626
+ if (m) max = Math.max(max, parseInt(m[1], 10) || 0);
6627
+ }
6628
+ return max;
6629
+ } catch {
6630
+ return 0;
6631
+ }
6632
+ }
6633
+ /** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
6634
+ formatAutonumber(format, value) {
6635
+ if (!format) return String(value);
6636
+ const m = format.match(/\{(0+)\}/);
6637
+ if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
6638
+ const padded = String(value).padStart(m[1].length, "0");
6639
+ return format.replace(m[0], padded);
6640
+ }
6514
6641
  /**
6515
6642
  * Register contribution (Manifest)
6516
6643
  *
@@ -6524,6 +6651,7 @@ var _ObjectQL = class _ObjectQL {
6524
6651
  registerApp(manifest) {
6525
6652
  const id = manifest.id || manifest.name;
6526
6653
  const namespace = manifest.namespace;
6654
+ this.invalidateSummaryIndex();
6527
6655
  this.logger.debug("Registering package manifest", { id, namespace });
6528
6656
  console.warn(`[ObjectQL:registerApp] id=${id} flows=${Array.isArray(manifest.flows) ? manifest.flows.length : typeof manifest.flows} keys=${Object.keys(manifest).join(",")}`);
6529
6657
  if (id) {
@@ -6604,6 +6732,7 @@ var _ObjectQL = class _ObjectQL {
6604
6732
  "pages",
6605
6733
  "dashboards",
6606
6734
  "reports",
6735
+ "datasets",
6607
6736
  "themes",
6608
6737
  // Automation Protocol
6609
6738
  "flows",
@@ -6748,6 +6877,7 @@ var _ObjectQL = class _ObjectQL {
6748
6877
  "pages",
6749
6878
  "dashboards",
6750
6879
  "reports",
6880
+ "datasets",
6751
6881
  "themes",
6752
6882
  "flows",
6753
6883
  "workflows",
@@ -7129,6 +7259,102 @@ var _ObjectQL = class _ObjectQL {
7129
7259
  }
7130
7260
  this.logger.info("ObjectQL engine destroyed");
7131
7261
  }
7262
+ /** Invalidate the cached roll-up summary index (call when metadata changes). */
7263
+ invalidateSummaryIndex() {
7264
+ this.summaryIndex = null;
7265
+ }
7266
+ /** Scan all registered objects for `summary` fields and index them by the
7267
+ * child object they aggregate, resolving the child→parent FK field. */
7268
+ buildSummaryIndex() {
7269
+ const index = /* @__PURE__ */ new Map();
7270
+ let objects = [];
7271
+ try {
7272
+ objects = this._registry.getAllObjects?.() ?? [];
7273
+ } catch {
7274
+ objects = [];
7275
+ }
7276
+ for (const parent of objects) {
7277
+ const fields = parent?.fields;
7278
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) continue;
7279
+ for (const [summaryField, def] of Object.entries(fields)) {
7280
+ const d = def;
7281
+ if (d?.type !== "summary" || !d.summaryOperations) continue;
7282
+ const so = d.summaryOperations;
7283
+ const childObject = so.object;
7284
+ const fn = so.function;
7285
+ if (!childObject || !fn) continue;
7286
+ let fkField = so.relationshipField;
7287
+ if (!fkField) {
7288
+ const child = this._registry.getObject(childObject);
7289
+ const cfields = child?.fields || {};
7290
+ for (const [cfName, cdef] of Object.entries(cfields)) {
7291
+ const cd = cdef;
7292
+ if ((cd?.type === "master_detail" || cd?.type === "lookup") && cd?.reference === parent.name) {
7293
+ fkField = cfName;
7294
+ break;
7295
+ }
7296
+ }
7297
+ }
7298
+ if (!fkField) continue;
7299
+ const list = index.get(childObject) ?? [];
7300
+ list.push({ parentObject: parent.name, summaryField, fkField, fn, sourceField: so.field });
7301
+ index.set(childObject, list);
7302
+ }
7303
+ }
7304
+ return index;
7305
+ }
7306
+ getSummaryDescriptors(childObject) {
7307
+ if (!this.summaryIndex) this.summaryIndex = this.buildSummaryIndex();
7308
+ return this.summaryIndex.get(childObject) ?? [];
7309
+ }
7310
+ /**
7311
+ * Recompute roll-up `summary` fields on parent records after a child write.
7312
+ * For each affected parent (the FK value on the changed/old child record), it
7313
+ * aggregates the child collection and writes the result onto the parent's
7314
+ * summary field. Runs in the caller's execution context so it joins the same
7315
+ * transaction (e.g. the cross-object batch) when one is open.
7316
+ */
7317
+ async recomputeSummaries(childObject, records, previous, execCtx) {
7318
+ const descriptors = this.getSummaryDescriptors(childObject);
7319
+ if (descriptors.length === 0) return;
7320
+ const recs = Array.isArray(records) ? records : records ? [records] : [];
7321
+ const prevs = Array.isArray(previous) ? previous : previous ? [previous] : [];
7322
+ for (const desc of descriptors) {
7323
+ const ids = /* @__PURE__ */ new Set();
7324
+ for (const r of recs) {
7325
+ const v = r?.[desc.fkField];
7326
+ if (v != null && v !== "") ids.add(String(v));
7327
+ }
7328
+ for (const p of prevs) {
7329
+ const v = p?.[desc.fkField];
7330
+ if (v != null && v !== "") ids.add(String(v));
7331
+ }
7332
+ for (const parentId of ids) {
7333
+ try {
7334
+ const rows = await this.aggregate(childObject, {
7335
+ where: { [desc.fkField]: parentId },
7336
+ aggregations: [{
7337
+ function: desc.fn,
7338
+ ...desc.fn === "count" ? {} : { field: desc.sourceField },
7339
+ alias: "value"
7340
+ }],
7341
+ context: execCtx
7342
+ });
7343
+ let value = rows?.[0]?.value;
7344
+ if (value == null) value = desc.fn === "count" || desc.fn === "sum" ? 0 : null;
7345
+ await this.update(desc.parentObject, { id: parentId, [desc.summaryField]: value }, { context: execCtx });
7346
+ } catch (err) {
7347
+ this.logger.warn("Roll-up summary recompute failed", {
7348
+ childObject,
7349
+ parentObject: desc.parentObject,
7350
+ parentId,
7351
+ field: desc.summaryField,
7352
+ error: err?.message
7353
+ });
7354
+ }
7355
+ }
7356
+ }
7357
+ }
7132
7358
  /**
7133
7359
  * Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
7134
7360
  *
@@ -7344,10 +7570,14 @@ var _ObjectQL = class _ObjectQL {
7344
7570
  let result;
7345
7571
  const nowSnap = /* @__PURE__ */ new Date();
7346
7572
  const schemaForValidation = this._registry.getObject(object);
7573
+ const driverOwnsAutonumber = driver?.supports?.autonumber === true;
7347
7574
  if (Array.isArray(hookContext.input.data)) {
7348
7575
  const rows = hookContext.input.data.map(
7349
7576
  (row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
7350
7577
  );
7578
+ for (const r of rows) {
7579
+ await this.applyAutonumbers(object, r, opCtx.context, driverOwnsAutonumber);
7580
+ }
7351
7581
  for (const r of rows) {
7352
7582
  await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
7353
7583
  }
@@ -7367,6 +7597,7 @@ var _ObjectQL = class _ObjectQL {
7367
7597
  opCtx.context,
7368
7598
  nowSnap
7369
7599
  );
7600
+ await this.applyAutonumbers(object, row, opCtx.context, driverOwnsAutonumber);
7370
7601
  await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
7371
7602
  validateRecord(schemaForValidation, row, "insert");
7372
7603
  evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
@@ -7375,6 +7606,7 @@ var _ObjectQL = class _ObjectQL {
7375
7606
  hookContext.event = "afterInsert";
7376
7607
  hookContext.result = result;
7377
7608
  await this.triggerHooks("afterInsert", hookContext);
7609
+ await this.recomputeSummaries(object, result, null, opCtx.context);
7378
7610
  if (this.realtimeService) {
7379
7611
  try {
7380
7612
  if (Array.isArray(result)) {
@@ -7459,6 +7691,7 @@ var _ObjectQL = class _ObjectQL {
7459
7691
  const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
7460
7692
  priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
7461
7693
  }
7694
+ hookContext.input.data = stripReadonlyWhenFields(updateSchema, hookContext.input.data, priorRecord, this.logger);
7462
7695
  evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
7463
7696
  result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
7464
7697
  } else if (options?.multi && driver.updateMany) {
@@ -7476,6 +7709,7 @@ var _ObjectQL = class _ObjectQL {
7476
7709
  hookContext.result = result;
7477
7710
  if (priorRecord) hookContext.previous = priorRecord;
7478
7711
  await this.triggerHooks("afterUpdate", hookContext);
7712
+ await this.recomputeSummaries(object, result, priorRecord, opCtx.context);
7479
7713
  if (this.realtimeService) {
7480
7714
  try {
7481
7715
  const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
@@ -7504,6 +7738,66 @@ var _ObjectQL = class _ObjectQL {
7504
7738
  });
7505
7739
  return opCtx.result;
7506
7740
  }
7741
+ /**
7742
+ * Apply referential delete behavior for relations pointing AT this record,
7743
+ * before it is removed. For every registered object with a `master_detail`
7744
+ * or `lookup` field referencing `object`, honor the field's `deleteBehavior`:
7745
+ * - `cascade` → delete the dependent rows (recursively, so grandchildren
7746
+ * are handled by each child's own delete),
7747
+ * - `set_null` → clear the foreign key,
7748
+ * - `restrict` → refuse the delete when dependents exist.
7749
+ * `master_detail` defaults to `cascade` (the parent owns the child
7750
+ * lifecycle); `lookup` defaults to `set_null`. Only runs for single-id
7751
+ * deletes — multi/predicate deletes skip cascade (logged).
7752
+ */
7753
+ async cascadeDeleteRelations(object, id, context, depth = 0) {
7754
+ if (id == null || depth >= _ObjectQL.MAX_CASCADE_DEPTH) return;
7755
+ let objects;
7756
+ try {
7757
+ objects = this._registry.getAllObjects();
7758
+ } catch {
7759
+ return;
7760
+ }
7761
+ for (const child of objects) {
7762
+ const childName = child?.name;
7763
+ const fields = child?.fields;
7764
+ if (!childName || !fields) continue;
7765
+ for (const [fieldName, fdef] of Object.entries(fields)) {
7766
+ if (!fdef || fdef.type !== "master_detail" && fdef.type !== "lookup") continue;
7767
+ const ref = fdef.reference;
7768
+ if (!ref) continue;
7769
+ let resolvedRef;
7770
+ try {
7771
+ resolvedRef = this.resolveObjectName(ref);
7772
+ } catch {
7773
+ resolvedRef = void 0;
7774
+ }
7775
+ if (ref !== object && resolvedRef !== object) continue;
7776
+ const behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
7777
+ let dependents;
7778
+ try {
7779
+ dependents = await this.find(childName, { where: { [fieldName]: id }, context });
7780
+ } catch {
7781
+ continue;
7782
+ }
7783
+ if (!dependents || dependents.length === 0) continue;
7784
+ if (behavior === "restrict") {
7785
+ throw new Error(
7786
+ `Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) via ${fieldName}`
7787
+ );
7788
+ }
7789
+ for (const dep of dependents) {
7790
+ const depId = dep?.id;
7791
+ if (depId == null) continue;
7792
+ if (behavior === "cascade") {
7793
+ await this.delete(childName, { where: { id: depId }, context });
7794
+ } else {
7795
+ await this.update(childName, { id: depId, [fieldName]: null }, { context });
7796
+ }
7797
+ }
7798
+ }
7799
+ }
7800
+ }
7507
7801
  async delete(object, options) {
7508
7802
  object = this.resolveObjectName(object);
7509
7803
  this.logger.debug("Delete operation starting", { object });
@@ -7533,7 +7827,15 @@ var _ObjectQL = class _ObjectQL {
7533
7827
  hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
7534
7828
  try {
7535
7829
  let result;
7830
+ let summaryPrev = null;
7831
+ if (hookContext.input.id && this.getSummaryDescriptors(object).length > 0) {
7832
+ try {
7833
+ summaryPrev = await this.findOne(object, { where: { id: hookContext.input.id }, context: opCtx.context });
7834
+ } catch {
7835
+ }
7836
+ }
7536
7837
  if (hookContext.input.id) {
7838
+ await this.cascadeDeleteRelations(object, hookContext.input.id, opCtx.context);
7537
7839
  result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
7538
7840
  } else if (options?.multi && driver.deleteMany) {
7539
7841
  const ast = { object, where: options.where };
@@ -7544,6 +7846,7 @@ var _ObjectQL = class _ObjectQL {
7544
7846
  hookContext.event = "afterDelete";
7545
7847
  hookContext.result = result;
7546
7848
  await this.triggerHooks("afterDelete", hookContext);
7849
+ if (summaryPrev) await this.recomputeSummaries(object, null, summaryPrev, opCtx.context);
7547
7850
  if (this.realtimeService) {
7548
7851
  try {
7549
7852
  const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
@@ -7693,7 +7996,7 @@ var _ObjectQL = class _ObjectQL {
7693
7996
  const trx = await drv.beginTransaction();
7694
7997
  const trxCtx = { ...baseContext ?? {}, transaction: trx };
7695
7998
  try {
7696
- const result = await callback(trxCtx);
7999
+ const result = await this.txStore.run({ transaction: trx }, () => callback(trxCtx));
7697
8000
  if (drv.commit) await drv.commit(trx);
7698
8001
  else if (drv.commitTransaction) await drv.commitTransaction(trx);
7699
8002
  return result;
@@ -7957,6 +8260,7 @@ var _ObjectQL = class _ObjectQL {
7957
8260
  // ============================================
7958
8261
  /** Maximum depth for recursive expand to prevent infinite loops */
7959
8262
  _ObjectQL.MAX_EXPAND_DEPTH = 3;
8263
+ _ObjectQL.MAX_CASCADE_DEPTH = 10;
7960
8264
  var ObjectQL = _ObjectQL;
7961
8265
  var ObjectRepository = class {
7962
8266
  constructor(objectName, context, engine) {
@@ -8074,8 +8378,10 @@ var ScopedContext = class _ScopedContext {
8074
8378
  { ...this.executionContext, transaction: trx },
8075
8379
  this.engine
8076
8380
  );
8381
+ const txStore = this.engine?.txStore;
8382
+ const runIn = (fn) => txStore ? txStore.run({ transaction: trx }, fn) : fn();
8077
8383
  try {
8078
- const result = await callback(trxCtx);
8384
+ const result = await runIn(() => callback(trxCtx));
8079
8385
  if (driver.commit) await driver.commit(trx);
8080
8386
  else if (driver.commitTransaction) await driver.commitTransaction(trx);
8081
8387
  return result;