@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.mjs CHANGED
@@ -5039,6 +5039,7 @@ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
5039
5039
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
5040
5040
 
5041
5041
  // src/engine.ts
5042
+ import { AsyncLocalStorage } from "async_hooks";
5042
5043
  import { ExecutionContextSchema } from "@objectstack/spec/kernel";
5043
5044
  import { createLogger } from "@objectstack/core";
5044
5045
  import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
@@ -5531,7 +5532,7 @@ function optionValues(options) {
5531
5532
  );
5532
5533
  }
5533
5534
  function validateOne(name, def, value) {
5534
- if (def.required && isMissing(value)) {
5535
+ if (def.required && isMissing(value) && def.type !== "autonumber") {
5535
5536
  return { field: name, code: "required", message: `${name} is required` };
5536
5537
  }
5537
5538
  if (isMissing(value)) return null;
@@ -5629,8 +5630,31 @@ var ajv = new Ajv({ allErrors: true, strict: false });
5629
5630
  var jsonSchemaCache = /* @__PURE__ */ new WeakMap();
5630
5631
  function needsPriorRecord(objectSchema) {
5631
5632
  const rules = objectSchema?.validations;
5632
- if (!Array.isArray(rules)) return false;
5633
- return rules.some((r) => ruleNeedsPrior(r));
5633
+ const ruleNeeds = Array.isArray(rules) && rules.some((r) => ruleNeedsPrior(r));
5634
+ return !!(ruleNeeds || fieldsNeedPrior(objectSchema?.fields));
5635
+ }
5636
+ function stripReadonlyWhenFields(objectSchema, data, previous, logger) {
5637
+ const fields = objectSchema?.fields;
5638
+ if (!fields || !data) return data;
5639
+ const merged = { ...previous ?? {}, ...data };
5640
+ let result = data;
5641
+ for (const [name, def] of Object.entries(fields)) {
5642
+ if (!def?.readonlyWhen || !(name in data)) continue;
5643
+ const res = ExpressionEngine2.evaluate(toExpression(def.readonlyWhen), {
5644
+ record: merged,
5645
+ previous: previous ?? void 0
5646
+ });
5647
+ if (!res.ok) {
5648
+ logger?.warn?.(`readonlyWhen for '${name}' failed to evaluate \u2014 change allowed through`);
5649
+ continue;
5650
+ }
5651
+ if (res.value === true) {
5652
+ if (result === data) result = { ...data };
5653
+ delete result[name];
5654
+ logger?.warn?.(`Field '${name}' is read-only (readonlyWhen) \u2014 ignoring incoming change`);
5655
+ }
5656
+ }
5657
+ return result;
5634
5658
  }
5635
5659
  function ruleNeedsPrior(r) {
5636
5660
  if (r == null || typeof r !== "object") return false;
@@ -5644,17 +5668,44 @@ function ruleNeedsPrior(r) {
5644
5668
  }
5645
5669
  return false;
5646
5670
  }
5671
+ function isMissing2(v) {
5672
+ return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
5673
+ }
5674
+ function fieldsNeedPrior(fields) {
5675
+ if (!fields) return false;
5676
+ return Object.values(fields).some(
5677
+ (f) => f && (f.requiredWhen || f.conditionalRequired || f.readonlyWhen)
5678
+ );
5679
+ }
5647
5680
  function toExpression(cond) {
5648
5681
  return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
5649
5682
  }
5650
5683
  function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5684
+ if (!data) return;
5651
5685
  const rules = objectSchema?.validations;
5652
- if (!Array.isArray(rules) || rules.length === 0 || !data) return;
5686
+ const hasRules = Array.isArray(rules) && rules.length > 0;
5687
+ const fields = objectSchema?.fields;
5688
+ const hasFieldRules = fieldsNeedPrior(fields);
5689
+ if (!hasRules && !hasFieldRules) return;
5653
5690
  const previous = opts.previous ?? void 0;
5654
5691
  const merged = { ...previous ?? {}, ...data };
5655
5692
  const ctx = { data, merged, previous, mode, logger: opts.logger };
5656
5693
  const errors = [];
5657
- const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5694
+ if (hasFieldRules && fields) {
5695
+ for (const [name, def] of Object.entries(fields)) {
5696
+ const pred = def?.requiredWhen ?? def?.conditionalRequired;
5697
+ if (!pred) continue;
5698
+ const res = ExpressionEngine2.evaluate(toExpression(pred), { record: merged, previous });
5699
+ if (!res.ok) {
5700
+ opts.logger?.warn?.(`requiredWhen for '${name}' failed to evaluate \u2014 skipped`);
5701
+ continue;
5702
+ }
5703
+ if (res.value === true && isMissing2(merged[name])) {
5704
+ errors.push({ field: name, code: "required", message: `${name} is required` });
5705
+ }
5706
+ }
5707
+ }
5708
+ const ordered = (hasRules ? rules : []).filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5658
5709
  const events = r.events ?? ["insert", "update"];
5659
5710
  return events.includes(mode);
5660
5711
  }).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
@@ -6032,6 +6083,15 @@ function resolveMetadataItemName(key, item) {
6032
6083
  }
6033
6084
  var _ObjectQL = class _ObjectQL {
6034
6085
  constructor(hostContext = {}) {
6086
+ /**
6087
+ * Ambient transaction store (ADR-0034). While a `transaction()` callback
6088
+ * runs, the active transaction handle lives here so that EVERY data
6089
+ * operation — including internal reads done during a write (reference
6090
+ * checks, hooks, expand) — automatically binds to the same connection
6091
+ * instead of asking the pool for another one and deadlocking on the
6092
+ * single-connection SQLite pool.
6093
+ */
6094
+ this.txStore = new AsyncLocalStorage();
6035
6095
  this.drivers = /* @__PURE__ */ new Map();
6036
6096
  this.defaultDriver = null;
6037
6097
  // Datasource mapping rules (imported from defineStack)
@@ -6073,6 +6133,12 @@ var _ObjectQL = class _ObjectQL {
6073
6133
  // getDriver()'s owner lookup would route CRUD to the wrong database. Each
6074
6134
  // engine now owns its registry so kernels are fully isolated.
6075
6135
  this._registry = new SchemaRegistry();
6136
+ /** In-memory next-value cache per `object.field` for autonumber generation,
6137
+ * lazily seeded from the current max in the store. */
6138
+ this.autonumberCounters = /* @__PURE__ */ new Map();
6139
+ /** Lazily-built index: child object name → roll-up summary descriptors on
6140
+ * parent objects that aggregate it. Invalidated when packages register. */
6141
+ this.summaryIndex = null;
6076
6142
  this.hostContext = hostContext;
6077
6143
  this.logger = hostContext.logger || createLogger({ level: "info", format: "pretty" });
6078
6144
  if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
@@ -6375,13 +6441,14 @@ var _ObjectQL = class _ObjectQL {
6375
6441
  * mask the system path.
6376
6442
  */
6377
6443
  buildDriverOptions(execCtx, base) {
6378
- const hasTx = execCtx?.transaction !== void 0;
6444
+ const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
6445
+ const hasTx = tx !== void 0;
6379
6446
  const hasTenant = execCtx?.tenantId !== void 0;
6380
6447
  const isSystem = execCtx?.isSystem === true;
6381
6448
  if (!hasTx && !hasTenant && !isSystem) return base;
6382
6449
  const opts = base && typeof base === "object" ? { ...base } : {};
6383
6450
  if (hasTx && opts.transaction === void 0) {
6384
- opts.transaction = execCtx.transaction;
6451
+ opts.transaction = tx;
6385
6452
  }
6386
6453
  if (hasTenant && opts.tenantId === void 0) {
6387
6454
  opts.tenantId = execCtx.tenantId;
@@ -6447,6 +6514,66 @@ var _ObjectQL = class _ObjectQL {
6447
6514
  }
6448
6515
  return out;
6449
6516
  }
6517
+ /**
6518
+ * Generate values for empty `autonumber` fields on insert — ONLY for drivers
6519
+ * that do not generate them natively (memory, mongodb). For SQL-backed objects
6520
+ * the driver owns a persistent, atomic `_objectstack_sequences` table and
6521
+ * advertises `supports.autonumber === true`; the engine then defers entirely
6522
+ * and never pre-fills (so the persistent sequence is the single source of
6523
+ * truth — see #1603). Required-validation exempts `autonumber` either way, so
6524
+ * a `required` record number is never rejected for "missing" — the runtime
6525
+ * owns the value, not the client.
6526
+ *
6527
+ * In the fallback path the next value is `max(existing) + 1`, seeded once per
6528
+ * `object.field` from the store then incremented in memory (monotonic within
6529
+ * the process, resilient to deletions). `autonumberFormat` is honored, e.g.
6530
+ * `CASE-{0000}` → `CASE-0042`. NOTE: this in-memory seeding is single-instance.
6531
+ */
6532
+ async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
6533
+ if (driverOwnsAutonumber) return;
6534
+ const fields = this.getSchema(object)?.fields;
6535
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
6536
+ for (const [name, def] of Object.entries(fields)) {
6537
+ if (def?.type !== "autonumber") continue;
6538
+ const current = record[name];
6539
+ if (current != null && current !== "") continue;
6540
+ const key = `${object}.${name}`;
6541
+ let next = this.autonumberCounters.get(key);
6542
+ if (next == null) next = await this.seedAutonumber(object, name, execCtx);
6543
+ next += 1;
6544
+ this.autonumberCounters.set(key, next);
6545
+ const fmt = def.autonumberFormat ?? def.format;
6546
+ record[name] = this.formatAutonumber(fmt, next);
6547
+ }
6548
+ }
6549
+ /** Seed the autonumber counter from the current max numeric value in store. */
6550
+ async seedAutonumber(object, field, execCtx) {
6551
+ try {
6552
+ const rows = await this.find(object, {
6553
+ select: ["id", field],
6554
+ limit: 5e3,
6555
+ context: execCtx
6556
+ });
6557
+ let max = 0;
6558
+ for (const r of rows || []) {
6559
+ const v = r?.[field];
6560
+ if (v == null) continue;
6561
+ const m = String(v).match(/(\d+)(?!.*\d)/);
6562
+ if (m) max = Math.max(max, parseInt(m[1], 10) || 0);
6563
+ }
6564
+ return max;
6565
+ } catch {
6566
+ return 0;
6567
+ }
6568
+ }
6569
+ /** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
6570
+ formatAutonumber(format, value) {
6571
+ if (!format) return String(value);
6572
+ const m = format.match(/\{(0+)\}/);
6573
+ if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
6574
+ const padded = String(value).padStart(m[1].length, "0");
6575
+ return format.replace(m[0], padded);
6576
+ }
6450
6577
  /**
6451
6578
  * Register contribution (Manifest)
6452
6579
  *
@@ -6460,6 +6587,7 @@ var _ObjectQL = class _ObjectQL {
6460
6587
  registerApp(manifest) {
6461
6588
  const id = manifest.id || manifest.name;
6462
6589
  const namespace = manifest.namespace;
6590
+ this.invalidateSummaryIndex();
6463
6591
  this.logger.debug("Registering package manifest", { id, namespace });
6464
6592
  console.warn(`[ObjectQL:registerApp] id=${id} flows=${Array.isArray(manifest.flows) ? manifest.flows.length : typeof manifest.flows} keys=${Object.keys(manifest).join(",")}`);
6465
6593
  if (id) {
@@ -6540,6 +6668,7 @@ var _ObjectQL = class _ObjectQL {
6540
6668
  "pages",
6541
6669
  "dashboards",
6542
6670
  "reports",
6671
+ "datasets",
6543
6672
  "themes",
6544
6673
  // Automation Protocol
6545
6674
  "flows",
@@ -6684,6 +6813,7 @@ var _ObjectQL = class _ObjectQL {
6684
6813
  "pages",
6685
6814
  "dashboards",
6686
6815
  "reports",
6816
+ "datasets",
6687
6817
  "themes",
6688
6818
  "flows",
6689
6819
  "workflows",
@@ -7065,6 +7195,102 @@ var _ObjectQL = class _ObjectQL {
7065
7195
  }
7066
7196
  this.logger.info("ObjectQL engine destroyed");
7067
7197
  }
7198
+ /** Invalidate the cached roll-up summary index (call when metadata changes). */
7199
+ invalidateSummaryIndex() {
7200
+ this.summaryIndex = null;
7201
+ }
7202
+ /** Scan all registered objects for `summary` fields and index them by the
7203
+ * child object they aggregate, resolving the child→parent FK field. */
7204
+ buildSummaryIndex() {
7205
+ const index = /* @__PURE__ */ new Map();
7206
+ let objects = [];
7207
+ try {
7208
+ objects = this._registry.getAllObjects?.() ?? [];
7209
+ } catch {
7210
+ objects = [];
7211
+ }
7212
+ for (const parent of objects) {
7213
+ const fields = parent?.fields;
7214
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) continue;
7215
+ for (const [summaryField, def] of Object.entries(fields)) {
7216
+ const d = def;
7217
+ if (d?.type !== "summary" || !d.summaryOperations) continue;
7218
+ const so = d.summaryOperations;
7219
+ const childObject = so.object;
7220
+ const fn = so.function;
7221
+ if (!childObject || !fn) continue;
7222
+ let fkField = so.relationshipField;
7223
+ if (!fkField) {
7224
+ const child = this._registry.getObject(childObject);
7225
+ const cfields = child?.fields || {};
7226
+ for (const [cfName, cdef] of Object.entries(cfields)) {
7227
+ const cd = cdef;
7228
+ if ((cd?.type === "master_detail" || cd?.type === "lookup") && cd?.reference === parent.name) {
7229
+ fkField = cfName;
7230
+ break;
7231
+ }
7232
+ }
7233
+ }
7234
+ if (!fkField) continue;
7235
+ const list = index.get(childObject) ?? [];
7236
+ list.push({ parentObject: parent.name, summaryField, fkField, fn, sourceField: so.field });
7237
+ index.set(childObject, list);
7238
+ }
7239
+ }
7240
+ return index;
7241
+ }
7242
+ getSummaryDescriptors(childObject) {
7243
+ if (!this.summaryIndex) this.summaryIndex = this.buildSummaryIndex();
7244
+ return this.summaryIndex.get(childObject) ?? [];
7245
+ }
7246
+ /**
7247
+ * Recompute roll-up `summary` fields on parent records after a child write.
7248
+ * For each affected parent (the FK value on the changed/old child record), it
7249
+ * aggregates the child collection and writes the result onto the parent's
7250
+ * summary field. Runs in the caller's execution context so it joins the same
7251
+ * transaction (e.g. the cross-object batch) when one is open.
7252
+ */
7253
+ async recomputeSummaries(childObject, records, previous, execCtx) {
7254
+ const descriptors = this.getSummaryDescriptors(childObject);
7255
+ if (descriptors.length === 0) return;
7256
+ const recs = Array.isArray(records) ? records : records ? [records] : [];
7257
+ const prevs = Array.isArray(previous) ? previous : previous ? [previous] : [];
7258
+ for (const desc of descriptors) {
7259
+ const ids = /* @__PURE__ */ new Set();
7260
+ for (const r of recs) {
7261
+ const v = r?.[desc.fkField];
7262
+ if (v != null && v !== "") ids.add(String(v));
7263
+ }
7264
+ for (const p of prevs) {
7265
+ const v = p?.[desc.fkField];
7266
+ if (v != null && v !== "") ids.add(String(v));
7267
+ }
7268
+ for (const parentId of ids) {
7269
+ try {
7270
+ const rows = await this.aggregate(childObject, {
7271
+ where: { [desc.fkField]: parentId },
7272
+ aggregations: [{
7273
+ function: desc.fn,
7274
+ ...desc.fn === "count" ? {} : { field: desc.sourceField },
7275
+ alias: "value"
7276
+ }],
7277
+ context: execCtx
7278
+ });
7279
+ let value = rows?.[0]?.value;
7280
+ if (value == null) value = desc.fn === "count" || desc.fn === "sum" ? 0 : null;
7281
+ await this.update(desc.parentObject, { id: parentId, [desc.summaryField]: value }, { context: execCtx });
7282
+ } catch (err) {
7283
+ this.logger.warn("Roll-up summary recompute failed", {
7284
+ childObject,
7285
+ parentObject: desc.parentObject,
7286
+ parentId,
7287
+ field: desc.summaryField,
7288
+ error: err?.message
7289
+ });
7290
+ }
7291
+ }
7292
+ }
7293
+ }
7068
7294
  /**
7069
7295
  * Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
7070
7296
  *
@@ -7280,10 +7506,14 @@ var _ObjectQL = class _ObjectQL {
7280
7506
  let result;
7281
7507
  const nowSnap = /* @__PURE__ */ new Date();
7282
7508
  const schemaForValidation = this._registry.getObject(object);
7509
+ const driverOwnsAutonumber = driver?.supports?.autonumber === true;
7283
7510
  if (Array.isArray(hookContext.input.data)) {
7284
7511
  const rows = hookContext.input.data.map(
7285
7512
  (row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
7286
7513
  );
7514
+ for (const r of rows) {
7515
+ await this.applyAutonumbers(object, r, opCtx.context, driverOwnsAutonumber);
7516
+ }
7287
7517
  for (const r of rows) {
7288
7518
  await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
7289
7519
  }
@@ -7303,6 +7533,7 @@ var _ObjectQL = class _ObjectQL {
7303
7533
  opCtx.context,
7304
7534
  nowSnap
7305
7535
  );
7536
+ await this.applyAutonumbers(object, row, opCtx.context, driverOwnsAutonumber);
7306
7537
  await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
7307
7538
  validateRecord(schemaForValidation, row, "insert");
7308
7539
  evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
@@ -7311,6 +7542,7 @@ var _ObjectQL = class _ObjectQL {
7311
7542
  hookContext.event = "afterInsert";
7312
7543
  hookContext.result = result;
7313
7544
  await this.triggerHooks("afterInsert", hookContext);
7545
+ await this.recomputeSummaries(object, result, null, opCtx.context);
7314
7546
  if (this.realtimeService) {
7315
7547
  try {
7316
7548
  if (Array.isArray(result)) {
@@ -7395,6 +7627,7 @@ var _ObjectQL = class _ObjectQL {
7395
7627
  const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
7396
7628
  priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
7397
7629
  }
7630
+ hookContext.input.data = stripReadonlyWhenFields(updateSchema, hookContext.input.data, priorRecord, this.logger);
7398
7631
  evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
7399
7632
  result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
7400
7633
  } else if (options?.multi && driver.updateMany) {
@@ -7412,6 +7645,7 @@ var _ObjectQL = class _ObjectQL {
7412
7645
  hookContext.result = result;
7413
7646
  if (priorRecord) hookContext.previous = priorRecord;
7414
7647
  await this.triggerHooks("afterUpdate", hookContext);
7648
+ await this.recomputeSummaries(object, result, priorRecord, opCtx.context);
7415
7649
  if (this.realtimeService) {
7416
7650
  try {
7417
7651
  const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
@@ -7440,6 +7674,66 @@ var _ObjectQL = class _ObjectQL {
7440
7674
  });
7441
7675
  return opCtx.result;
7442
7676
  }
7677
+ /**
7678
+ * Apply referential delete behavior for relations pointing AT this record,
7679
+ * before it is removed. For every registered object with a `master_detail`
7680
+ * or `lookup` field referencing `object`, honor the field's `deleteBehavior`:
7681
+ * - `cascade` → delete the dependent rows (recursively, so grandchildren
7682
+ * are handled by each child's own delete),
7683
+ * - `set_null` → clear the foreign key,
7684
+ * - `restrict` → refuse the delete when dependents exist.
7685
+ * `master_detail` defaults to `cascade` (the parent owns the child
7686
+ * lifecycle); `lookup` defaults to `set_null`. Only runs for single-id
7687
+ * deletes — multi/predicate deletes skip cascade (logged).
7688
+ */
7689
+ async cascadeDeleteRelations(object, id, context, depth = 0) {
7690
+ if (id == null || depth >= _ObjectQL.MAX_CASCADE_DEPTH) return;
7691
+ let objects;
7692
+ try {
7693
+ objects = this._registry.getAllObjects();
7694
+ } catch {
7695
+ return;
7696
+ }
7697
+ for (const child of objects) {
7698
+ const childName = child?.name;
7699
+ const fields = child?.fields;
7700
+ if (!childName || !fields) continue;
7701
+ for (const [fieldName, fdef] of Object.entries(fields)) {
7702
+ if (!fdef || fdef.type !== "master_detail" && fdef.type !== "lookup") continue;
7703
+ const ref = fdef.reference;
7704
+ if (!ref) continue;
7705
+ let resolvedRef;
7706
+ try {
7707
+ resolvedRef = this.resolveObjectName(ref);
7708
+ } catch {
7709
+ resolvedRef = void 0;
7710
+ }
7711
+ if (ref !== object && resolvedRef !== object) continue;
7712
+ const behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
7713
+ let dependents;
7714
+ try {
7715
+ dependents = await this.find(childName, { where: { [fieldName]: id }, context });
7716
+ } catch {
7717
+ continue;
7718
+ }
7719
+ if (!dependents || dependents.length === 0) continue;
7720
+ if (behavior === "restrict") {
7721
+ throw new Error(
7722
+ `Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) via ${fieldName}`
7723
+ );
7724
+ }
7725
+ for (const dep of dependents) {
7726
+ const depId = dep?.id;
7727
+ if (depId == null) continue;
7728
+ if (behavior === "cascade") {
7729
+ await this.delete(childName, { where: { id: depId }, context });
7730
+ } else {
7731
+ await this.update(childName, { id: depId, [fieldName]: null }, { context });
7732
+ }
7733
+ }
7734
+ }
7735
+ }
7736
+ }
7443
7737
  async delete(object, options) {
7444
7738
  object = this.resolveObjectName(object);
7445
7739
  this.logger.debug("Delete operation starting", { object });
@@ -7469,7 +7763,15 @@ var _ObjectQL = class _ObjectQL {
7469
7763
  hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
7470
7764
  try {
7471
7765
  let result;
7766
+ let summaryPrev = null;
7767
+ if (hookContext.input.id && this.getSummaryDescriptors(object).length > 0) {
7768
+ try {
7769
+ summaryPrev = await this.findOne(object, { where: { id: hookContext.input.id }, context: opCtx.context });
7770
+ } catch {
7771
+ }
7772
+ }
7472
7773
  if (hookContext.input.id) {
7774
+ await this.cascadeDeleteRelations(object, hookContext.input.id, opCtx.context);
7473
7775
  result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
7474
7776
  } else if (options?.multi && driver.deleteMany) {
7475
7777
  const ast = { object, where: options.where };
@@ -7480,6 +7782,7 @@ var _ObjectQL = class _ObjectQL {
7480
7782
  hookContext.event = "afterDelete";
7481
7783
  hookContext.result = result;
7482
7784
  await this.triggerHooks("afterDelete", hookContext);
7785
+ if (summaryPrev) await this.recomputeSummaries(object, null, summaryPrev, opCtx.context);
7483
7786
  if (this.realtimeService) {
7484
7787
  try {
7485
7788
  const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
@@ -7629,7 +7932,7 @@ var _ObjectQL = class _ObjectQL {
7629
7932
  const trx = await drv.beginTransaction();
7630
7933
  const trxCtx = { ...baseContext ?? {}, transaction: trx };
7631
7934
  try {
7632
- const result = await callback(trxCtx);
7935
+ const result = await this.txStore.run({ transaction: trx }, () => callback(trxCtx));
7633
7936
  if (drv.commit) await drv.commit(trx);
7634
7937
  else if (drv.commitTransaction) await drv.commitTransaction(trx);
7635
7938
  return result;
@@ -7893,6 +8196,7 @@ var _ObjectQL = class _ObjectQL {
7893
8196
  // ============================================
7894
8197
  /** Maximum depth for recursive expand to prevent infinite loops */
7895
8198
  _ObjectQL.MAX_EXPAND_DEPTH = 3;
8199
+ _ObjectQL.MAX_CASCADE_DEPTH = 10;
7896
8200
  var ObjectQL = _ObjectQL;
7897
8201
  var ObjectRepository = class {
7898
8202
  constructor(objectName, context, engine) {
@@ -8010,8 +8314,10 @@ var ScopedContext = class _ScopedContext {
8010
8314
  { ...this.executionContext, transaction: trx },
8011
8315
  this.engine
8012
8316
  );
8317
+ const txStore = this.engine?.txStore;
8318
+ const runIn = (fn) => txStore ? txStore.run({ transaction: trx }, fn) : fn();
8013
8319
  try {
8014
- const result = await callback(trxCtx);
8320
+ const result = await runIn(() => callback(trxCtx));
8015
8321
  if (driver.commit) await driver.commit(trx);
8016
8322
  else if (driver.commitTransaction) await driver.commitTransaction(trx);
8017
8323
  return result;