@objectstack/objectql 9.9.1 → 9.11.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.d.mts CHANGED
@@ -141,6 +141,22 @@ interface SchemaRegistryOptions {
141
141
  * timestamps; the `*_by` lookups are filled by the runtime when an
142
142
  * authenticated session is present (NULL otherwise — e.g. seeded
143
143
  * rows).
144
+ * - `owner_id` — canonical, *reassignable* record owner (lookup to
145
+ * `sys_user`). Auto-provisioned by DEFAULT on user-authored business
146
+ * objects so ownership is correct-by-default for AI/human authors who
147
+ * would otherwise forget to declare it (or reinvent a custom `owner`
148
+ * lookup the platform can't see). Once present, the existing machinery
149
+ * engages automatically: SecurityPlugin auto-stamps it to the acting
150
+ * user on insert (step 3.5), owner-scoped RLS / "My" views / owner
151
+ * reports key off it, and the first-admin bootstrap hands seeded rows
152
+ * (owner_id NULL) to the promoted admin. Unlike `created_by` it is
153
+ * editable (`readonly: false`) — ownership transfers; provenance does
154
+ * not. Excluded for `managedBy` / `sys_*` tables and any object that
155
+ * opts out via `ownership: 'org' | 'none'` (Dataverse-style: a
156
+ * catalog/junction table that has no per-record owner). Forgetting the
157
+ * opt-out is harmless (a spare nullable column), whereas forgetting to
158
+ * ADD ownership — the failure mode we are eliminating — silently breaks
159
+ * every owner-keyed feature.
144
160
  */
145
161
  declare function applySystemFields(schema: ServiceObject, opts: {
146
162
  multiTenant: boolean;
@@ -612,7 +628,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
612
628
  } | {
613
629
  language: "js";
614
630
  source: string;
615
- capabilities: ("log" | "api.read" | "api.write" | "crypto.uuid" | "crypto.hash")[];
631
+ capabilities: ("log" | "api.read" | "api.write" | "api.transaction" | "crypto.uuid" | "crypto.hash")[];
616
632
  timeoutMs?: number | undefined;
617
633
  memoryMb?: number | undefined;
618
634
  } | undefined;
@@ -2209,15 +2225,22 @@ declare class ObjectQL implements IDataEngine {
2209
2225
  * owns the value, not the client.
2210
2226
  *
2211
2227
  * In the fallback path the next value is `max(existing) + 1`, seeded once per
2212
- * `object.field` from the store then incremented in memory (monotonic within
2213
- * the process, resilient to deletions). `autonumberFormat` is honored, e.g.
2214
- * `CASE-{0000}` `CASE-0042`. NOTE: this in-memory seeding is single-instance.
2228
+ * `object.field.<scope>` from the store then incremented in memory (monotonic
2229
+ * within the process, resilient to deletions). The shared `autonumberFormat`
2230
+ * renderer is honored end-to-end, so date tokens (`AD{YYYYMMDD}{0000}`), field
2231
+ * interpolation (`{island_zone}{000}`) and per-scope reset behave identically
2232
+ * to the SQL driver's persistent sequence (#1603). NOTE: this in-memory seeding
2233
+ * is single-instance.
2215
2234
  */
2216
2235
  private applyAutonumbers;
2217
- /** Seed the autonumber counter from the current max numeric value in store. */
2236
+ /**
2237
+ * Seed the autonumber counter from the current max in store, scoped to
2238
+ * `prefix`. With a non-empty prefix (date/field formats) only rows in the
2239
+ * same scope count, and the counter is the digit-run immediately after the
2240
+ * prefix; with an empty prefix (legacy fixed-prefix formats) the last digit
2241
+ * run of the whole value is used, preserving the original behaviour.
2242
+ */
2218
2243
  private seedAutonumber;
2219
- /** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
2220
- private formatAutonumber;
2221
2244
  /**
2222
2245
  * Register contribution (Manifest)
2223
2246
  *
@@ -2664,6 +2687,38 @@ declare class ScopedContext {
2664
2687
  * does not support transactions.
2665
2688
  */
2666
2689
  transaction(callback: (trxCtx: ScopedContext) => Promise<any>): Promise<any>;
2690
+ /**
2691
+ * Resolve the default driver, if it exposes transaction primitives.
2692
+ * Shared by {@link transaction} and the discrete begin/commit/rollback trio.
2693
+ */
2694
+ private txDriver;
2695
+ /**
2696
+ * Discrete transaction primitives — `begin` / `commit` / `rollback` as three
2697
+ * separate calls, in contrast to {@link transaction}'s single-callback form.
2698
+ *
2699
+ * This trio exists for callers that cannot keep a JS closure on the stack for
2700
+ * the lifetime of the transaction — chiefly the sandbox runner, where the
2701
+ * hook/action body's `ctx.api.transaction(fn)` is driven across many host
2702
+ * event-loop turns via deferred promises. Across those `setImmediate`
2703
+ * boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
2704
+ * survive, so the transaction handle is threaded **explicitly**: `begin`
2705
+ * returns a child ScopedContext carrying `transaction: trx` in its execution
2706
+ * context, and `resolveTx` honors that explicit handle ahead of the ambient
2707
+ * store. Every `object(...)` op on the returned context therefore reuses the
2708
+ * one connection without relying on ALS.
2709
+ *
2710
+ * Returns `null` when the driver has no transaction support — the caller then
2711
+ * runs non-transactionally against `this` (same graceful degrade as
2712
+ * {@link transaction}).
2713
+ */
2714
+ beginTransaction(): Promise<{
2715
+ ctx: ScopedContext;
2716
+ handle: unknown;
2717
+ } | null>;
2718
+ /** Commit a handle obtained from {@link beginTransaction}. */
2719
+ commitTransaction(handle: unknown): Promise<void>;
2720
+ /** Roll back a handle obtained from {@link beginTransaction}. */
2721
+ rollbackTransaction(handle: unknown): Promise<void>;
2667
2722
  get userId(): string | undefined;
2668
2723
  get tenantId(): string | undefined;
2669
2724
  /** Alias for tenantId — matches ObjectQLContext.spaceId convention */
package/dist/index.d.ts CHANGED
@@ -141,6 +141,22 @@ interface SchemaRegistryOptions {
141
141
  * timestamps; the `*_by` lookups are filled by the runtime when an
142
142
  * authenticated session is present (NULL otherwise — e.g. seeded
143
143
  * rows).
144
+ * - `owner_id` — canonical, *reassignable* record owner (lookup to
145
+ * `sys_user`). Auto-provisioned by DEFAULT on user-authored business
146
+ * objects so ownership is correct-by-default for AI/human authors who
147
+ * would otherwise forget to declare it (or reinvent a custom `owner`
148
+ * lookup the platform can't see). Once present, the existing machinery
149
+ * engages automatically: SecurityPlugin auto-stamps it to the acting
150
+ * user on insert (step 3.5), owner-scoped RLS / "My" views / owner
151
+ * reports key off it, and the first-admin bootstrap hands seeded rows
152
+ * (owner_id NULL) to the promoted admin. Unlike `created_by` it is
153
+ * editable (`readonly: false`) — ownership transfers; provenance does
154
+ * not. Excluded for `managedBy` / `sys_*` tables and any object that
155
+ * opts out via `ownership: 'org' | 'none'` (Dataverse-style: a
156
+ * catalog/junction table that has no per-record owner). Forgetting the
157
+ * opt-out is harmless (a spare nullable column), whereas forgetting to
158
+ * ADD ownership — the failure mode we are eliminating — silently breaks
159
+ * every owner-keyed feature.
144
160
  */
145
161
  declare function applySystemFields(schema: ServiceObject, opts: {
146
162
  multiTenant: boolean;
@@ -612,7 +628,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
612
628
  } | {
613
629
  language: "js";
614
630
  source: string;
615
- capabilities: ("log" | "api.read" | "api.write" | "crypto.uuid" | "crypto.hash")[];
631
+ capabilities: ("log" | "api.read" | "api.write" | "api.transaction" | "crypto.uuid" | "crypto.hash")[];
616
632
  timeoutMs?: number | undefined;
617
633
  memoryMb?: number | undefined;
618
634
  } | undefined;
@@ -2209,15 +2225,22 @@ declare class ObjectQL implements IDataEngine {
2209
2225
  * owns the value, not the client.
2210
2226
  *
2211
2227
  * In the fallback path the next value is `max(existing) + 1`, seeded once per
2212
- * `object.field` from the store then incremented in memory (monotonic within
2213
- * the process, resilient to deletions). `autonumberFormat` is honored, e.g.
2214
- * `CASE-{0000}` `CASE-0042`. NOTE: this in-memory seeding is single-instance.
2228
+ * `object.field.<scope>` from the store then incremented in memory (monotonic
2229
+ * within the process, resilient to deletions). The shared `autonumberFormat`
2230
+ * renderer is honored end-to-end, so date tokens (`AD{YYYYMMDD}{0000}`), field
2231
+ * interpolation (`{island_zone}{000}`) and per-scope reset behave identically
2232
+ * to the SQL driver's persistent sequence (#1603). NOTE: this in-memory seeding
2233
+ * is single-instance.
2215
2234
  */
2216
2235
  private applyAutonumbers;
2217
- /** Seed the autonumber counter from the current max numeric value in store. */
2236
+ /**
2237
+ * Seed the autonumber counter from the current max in store, scoped to
2238
+ * `prefix`. With a non-empty prefix (date/field formats) only rows in the
2239
+ * same scope count, and the counter is the digit-run immediately after the
2240
+ * prefix; with an empty prefix (legacy fixed-prefix formats) the last digit
2241
+ * run of the whole value is used, preserving the original behaviour.
2242
+ */
2218
2243
  private seedAutonumber;
2219
- /** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
2220
- private formatAutonumber;
2221
2244
  /**
2222
2245
  * Register contribution (Manifest)
2223
2246
  *
@@ -2664,6 +2687,38 @@ declare class ScopedContext {
2664
2687
  * does not support transactions.
2665
2688
  */
2666
2689
  transaction(callback: (trxCtx: ScopedContext) => Promise<any>): Promise<any>;
2690
+ /**
2691
+ * Resolve the default driver, if it exposes transaction primitives.
2692
+ * Shared by {@link transaction} and the discrete begin/commit/rollback trio.
2693
+ */
2694
+ private txDriver;
2695
+ /**
2696
+ * Discrete transaction primitives — `begin` / `commit` / `rollback` as three
2697
+ * separate calls, in contrast to {@link transaction}'s single-callback form.
2698
+ *
2699
+ * This trio exists for callers that cannot keep a JS closure on the stack for
2700
+ * the lifetime of the transaction — chiefly the sandbox runner, where the
2701
+ * hook/action body's `ctx.api.transaction(fn)` is driven across many host
2702
+ * event-loop turns via deferred promises. Across those `setImmediate`
2703
+ * boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
2704
+ * survive, so the transaction handle is threaded **explicitly**: `begin`
2705
+ * returns a child ScopedContext carrying `transaction: trx` in its execution
2706
+ * context, and `resolveTx` honors that explicit handle ahead of the ambient
2707
+ * store. Every `object(...)` op on the returned context therefore reuses the
2708
+ * one connection without relying on ALS.
2709
+ *
2710
+ * Returns `null` when the driver has no transaction support — the caller then
2711
+ * runs non-transactionally against `this` (same graceful degrade as
2712
+ * {@link transaction}).
2713
+ */
2714
+ beginTransaction(): Promise<{
2715
+ ctx: ScopedContext;
2716
+ handle: unknown;
2717
+ } | null>;
2718
+ /** Commit a handle obtained from {@link beginTransaction}. */
2719
+ commitTransaction(handle: unknown): Promise<void>;
2720
+ /** Roll back a handle obtained from {@link beginTransaction}. */
2721
+ rollbackTransaction(handle: unknown): Promise<void>;
2667
2722
  get userId(): string | undefined;
2668
2723
  get tenantId(): string | undefined;
2669
2724
  /** Alias for tenantId — matches ObjectQLContext.spaceId convention */
package/dist/index.js CHANGED
@@ -163,7 +163,10 @@ var init_seed_loader = __esm({
163
163
  const seedIdentity = config.identity;
164
164
  const baseEvalCtx = {
165
165
  now: seedNow,
166
- user: seedIdentity?.user,
166
+ // `id: null` is a legitimate seed-time state (the owning admin does not
167
+ // exist yet) that the formula EvalContext's `user.id: string` type does
168
+ // not yet model — cast the fallback so `os.user.id` evaluates to null.
169
+ user: seedIdentity?.user ?? { id: null },
167
170
  // Fall back to the per-tenant organizationId so `os.org.id` resolves
168
171
  // during per-org replay even without an explicit identity.org.
169
172
  org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
@@ -183,7 +186,7 @@ var init_seed_loader = __esm({
183
186
  targetField: "(expression)",
184
187
  attemptedValue: dataset.records[i],
185
188
  recordIndex: i,
186
- message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. Records using cel\`os.user.id\` / cel\`os.org.id\` require a seed identity \u2014 ensure a system/admin user exists before seeding (see SeedLoaderConfig.identity).`
189
+ message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. \`os.user.id\` resolves to null at seed time (the owning admin does not exist yet) and owner-style fields are assigned by the first-admin handoff \u2014 so a required, non-owner field must not depend on it. Provide a literal value or make the field optional.`
187
190
  };
188
191
  errors.push(error);
189
192
  allErrors.push(error);
@@ -904,6 +907,8 @@ function applySystemFields(schema, opts) {
904
907
  const tenancyDisabled = schema.tenancy?.enabled === false;
905
908
  const wantTenant = sf?.tenant !== false && !tenancyDisabled;
906
909
  const wantAudit = sf?.audit !== false;
910
+ const ownership = schema.ownership;
911
+ const wantOwner = ownership !== "org" && ownership !== "none" && !schema.managedBy && !schema.name.startsWith("sys_");
907
912
  const additions = {};
908
913
  if (wantTenant && !schema.fields?.organization_id) {
909
914
  additions.organization_id = {
@@ -962,6 +967,17 @@ function applySystemFields(schema, opts) {
962
967
  };
963
968
  }
964
969
  }
970
+ if (wantOwner && !schema.fields?.owner_id) {
971
+ additions.owner_id = {
972
+ type: "lookup",
973
+ reference: "sys_user",
974
+ label: "Owner",
975
+ required: false,
976
+ readonly: false,
977
+ system: true,
978
+ description: "Record owner (auto-stamped to the creating user on insert; reassignable). Drives owner-scoped views, reports and notifications."
979
+ };
980
+ }
965
981
  if (Object.keys(additions).length === 0) return schema;
966
982
  return {
967
983
  ...schema,
@@ -6176,6 +6192,7 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
6176
6192
 
6177
6193
  // src/engine.ts
6178
6194
  var import_node_async_hooks = require("async_hooks");
6195
+ var import_data4 = require("@objectstack/spec/data");
6179
6196
  var import_kernel6 = require("@objectstack/spec/kernel");
6180
6197
  var import_core2 = require("@objectstack/core");
6181
6198
  var import_system2 = require("@objectstack/spec/system");
@@ -7084,7 +7101,7 @@ function aggregateBucket(rows, aggregations) {
7084
7101
  const alias = agg.alias;
7085
7102
  const fn = agg.function;
7086
7103
  if (fn === "count") {
7087
- if (!agg.field) {
7104
+ if (!agg.field || agg.field === "*") {
7088
7105
  out[alias] = rows.length;
7089
7106
  } else {
7090
7107
  out[alias] = rows.reduce(
@@ -7601,8 +7618,9 @@ var _ObjectQL = class _ObjectQL {
7601
7618
  const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
7602
7619
  const hasTx = tx !== void 0;
7603
7620
  const hasTenant = execCtx?.tenantId !== void 0;
7621
+ const hasTz = execCtx?.timezone !== void 0;
7604
7622
  const isSystem = execCtx?.isSystem === true;
7605
- if (!hasTx && !hasTenant && !isSystem) return base;
7623
+ if (!hasTx && !hasTenant && !isSystem && !hasTz) return base;
7606
7624
  const opts = base && typeof base === "object" ? { ...base } : {};
7607
7625
  if (hasTx && opts.transaction === void 0) {
7608
7626
  opts.transaction = tx;
@@ -7610,6 +7628,9 @@ var _ObjectQL = class _ObjectQL {
7610
7628
  if (hasTenant && opts.tenantId === void 0) {
7611
7629
  opts.tenantId = execCtx.tenantId;
7612
7630
  }
7631
+ if (hasTz && opts.timezone === void 0) {
7632
+ opts.timezone = execCtx.timezone;
7633
+ }
7613
7634
  if (isSystem && opts.bypassTenantAudit === void 0) {
7614
7635
  opts.bypassTenantAudit = true;
7615
7636
  }
@@ -7683,29 +7704,48 @@ var _ObjectQL = class _ObjectQL {
7683
7704
  * owns the value, not the client.
7684
7705
  *
7685
7706
  * In the fallback path the next value is `max(existing) + 1`, seeded once per
7686
- * `object.field` from the store then incremented in memory (monotonic within
7687
- * the process, resilient to deletions). `autonumberFormat` is honored, e.g.
7688
- * `CASE-{0000}` `CASE-0042`. NOTE: this in-memory seeding is single-instance.
7707
+ * `object.field.<scope>` from the store then incremented in memory (monotonic
7708
+ * within the process, resilient to deletions). The shared `autonumberFormat`
7709
+ * renderer is honored end-to-end, so date tokens (`AD{YYYYMMDD}{0000}`), field
7710
+ * interpolation (`{island_zone}{000}`) and per-scope reset behave identically
7711
+ * to the SQL driver's persistent sequence (#1603). NOTE: this in-memory seeding
7712
+ * is single-instance.
7689
7713
  */
7690
7714
  async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
7691
7715
  if (driverOwnsAutonumber) return;
7692
7716
  const fields = this.getSchema(object)?.fields;
7693
7717
  if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
7718
+ const now = /* @__PURE__ */ new Date();
7719
+ const timezone = execCtx?.timezone;
7694
7720
  for (const [name, def] of Object.entries(fields)) {
7695
7721
  if (def?.type !== "autonumber") continue;
7696
7722
  const current = record[name];
7697
7723
  if (current != null && current !== "") continue;
7698
- const key = `${object}.${name}`;
7699
- let next = this.autonumberCounters.get(key);
7700
- if (next == null) next = await this.seedAutonumber(object, name, execCtx);
7701
- next += 1;
7702
- this.autonumberCounters.set(key, next);
7703
7724
  const fmt = def.autonumberFormat ?? def.format;
7704
- record[name] = this.formatAutonumber(fmt, next);
7725
+ const tokens = (0, import_data4.parseAutonumberFormat)(typeof fmt === "string" ? fmt : "");
7726
+ const missing = (0, import_data4.missingFieldValues)(tokens, record);
7727
+ if (missing.length > 0) {
7728
+ throw new Error(
7729
+ `Cannot generate autonumber "${object}.${name}" (format "${fmt}"): referenced field(s) [${missing.join(", ")}] are empty on the record. Fields interpolated into an autonumber format must be set before the record is created.`
7730
+ );
7731
+ }
7732
+ const probe = (0, import_data4.renderAutonumber)({ tokens, seq: 0, record, now, timezone });
7733
+ const counterKey = `${object}.${name}.${probe.scope}`;
7734
+ let next = this.autonumberCounters.get(counterKey);
7735
+ if (next == null) next = await this.seedAutonumber(object, name, probe.prefix, execCtx);
7736
+ next += 1;
7737
+ this.autonumberCounters.set(counterKey, next);
7738
+ record[name] = (0, import_data4.renderAutonumber)({ tokens, seq: next, record, now, timezone }).value;
7705
7739
  }
7706
7740
  }
7707
- /** Seed the autonumber counter from the current max numeric value in store. */
7708
- async seedAutonumber(object, field, execCtx) {
7741
+ /**
7742
+ * Seed the autonumber counter from the current max in store, scoped to
7743
+ * `prefix`. With a non-empty prefix (date/field formats) only rows in the
7744
+ * same scope count, and the counter is the digit-run immediately after the
7745
+ * prefix; with an empty prefix (legacy fixed-prefix formats) the last digit
7746
+ * run of the whole value is used, preserving the original behaviour.
7747
+ */
7748
+ async seedAutonumber(object, field, prefix, execCtx) {
7709
7749
  try {
7710
7750
  const rows = await this.find(object, {
7711
7751
  select: ["id", field],
@@ -7716,22 +7756,24 @@ var _ObjectQL = class _ObjectQL {
7716
7756
  for (const r of rows || []) {
7717
7757
  const v = r?.[field];
7718
7758
  if (v == null) continue;
7719
- const m = String(v).match(/(\d+)(?!.*\d)/);
7720
- if (m) max = Math.max(max, parseInt(m[1], 10) || 0);
7759
+ const s = String(v);
7760
+ if (prefix && !s.startsWith(prefix)) continue;
7761
+ const tail = prefix ? s.slice(prefix.length) : s;
7762
+ let digits;
7763
+ if (prefix) {
7764
+ const head = tail.match(/^\d+/);
7765
+ digits = head ? head[0] : void 0;
7766
+ } else {
7767
+ const runs = tail.match(/\d+/g);
7768
+ digits = runs ? runs[runs.length - 1] : void 0;
7769
+ }
7770
+ if (digits) max = Math.max(max, parseInt(digits, 10) || 0);
7721
7771
  }
7722
7772
  return max;
7723
7773
  } catch {
7724
7774
  return 0;
7725
7775
  }
7726
7776
  }
7727
- /** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
7728
- formatAutonumber(format, value) {
7729
- if (!format) return String(value);
7730
- const m = format.match(/\{(0+)\}/);
7731
- if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
7732
- const padded = String(value).padStart(m[1].length, "0");
7733
- return format.replace(m[0], padded);
7734
- }
7735
7777
  /**
7736
7778
  * Register contribution (Manifest)
7737
7779
  *
@@ -9507,6 +9549,58 @@ var ScopedContext = class _ScopedContext {
9507
9549
  throw error;
9508
9550
  }
9509
9551
  }
9552
+ /**
9553
+ * Resolve the default driver, if it exposes transaction primitives.
9554
+ * Shared by {@link transaction} and the discrete begin/commit/rollback trio.
9555
+ */
9556
+ txDriver() {
9557
+ const engine = this.engine;
9558
+ const driver = engine.defaultDriver ? engine.drivers?.get(engine.defaultDriver) : void 0;
9559
+ return driver?.beginTransaction ? driver : void 0;
9560
+ }
9561
+ /**
9562
+ * Discrete transaction primitives — `begin` / `commit` / `rollback` as three
9563
+ * separate calls, in contrast to {@link transaction}'s single-callback form.
9564
+ *
9565
+ * This trio exists for callers that cannot keep a JS closure on the stack for
9566
+ * the lifetime of the transaction — chiefly the sandbox runner, where the
9567
+ * hook/action body's `ctx.api.transaction(fn)` is driven across many host
9568
+ * event-loop turns via deferred promises. Across those `setImmediate`
9569
+ * boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
9570
+ * survive, so the transaction handle is threaded **explicitly**: `begin`
9571
+ * returns a child ScopedContext carrying `transaction: trx` in its execution
9572
+ * context, and `resolveTx` honors that explicit handle ahead of the ambient
9573
+ * store. Every `object(...)` op on the returned context therefore reuses the
9574
+ * one connection without relying on ALS.
9575
+ *
9576
+ * Returns `null` when the driver has no transaction support — the caller then
9577
+ * runs non-transactionally against `this` (same graceful degrade as
9578
+ * {@link transaction}).
9579
+ */
9580
+ async beginTransaction() {
9581
+ const driver = this.txDriver();
9582
+ if (!driver) return null;
9583
+ const trx = await driver.beginTransaction();
9584
+ const ctx = new _ScopedContext(
9585
+ { ...this.executionContext, transaction: trx },
9586
+ this.engine
9587
+ );
9588
+ return { ctx, handle: trx };
9589
+ }
9590
+ /** Commit a handle obtained from {@link beginTransaction}. */
9591
+ async commitTransaction(handle) {
9592
+ const driver = this.txDriver();
9593
+ if (!driver) return;
9594
+ if (driver.commit) await driver.commit(handle);
9595
+ else if (driver.commitTransaction) await driver.commitTransaction(handle);
9596
+ }
9597
+ /** Roll back a handle obtained from {@link beginTransaction}. */
9598
+ async rollbackTransaction(handle) {
9599
+ const driver = this.txDriver();
9600
+ if (!driver) return;
9601
+ if (driver.rollback) await driver.rollback(handle);
9602
+ else if (driver.rollbackTransaction) await driver.rollbackTransaction(handle);
9603
+ }
9510
9604
  get userId() {
9511
9605
  return this.executionContext.userId;
9512
9606
  }