@objectstack/objectql 9.10.0 → 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.mjs CHANGED
@@ -6133,6 +6133,7 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
6133
6133
 
6134
6134
  // src/engine.ts
6135
6135
  import { AsyncLocalStorage } from "async_hooks";
6136
+ import { parseAutonumberFormat, renderAutonumber, missingFieldValues } from "@objectstack/spec/data";
6136
6137
  import { ExecutionContextSchema } from "@objectstack/spec/kernel";
6137
6138
  import { createLogger } from "@objectstack/core";
6138
6139
  import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
@@ -7558,8 +7559,9 @@ var _ObjectQL = class _ObjectQL {
7558
7559
  const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
7559
7560
  const hasTx = tx !== void 0;
7560
7561
  const hasTenant = execCtx?.tenantId !== void 0;
7562
+ const hasTz = execCtx?.timezone !== void 0;
7561
7563
  const isSystem = execCtx?.isSystem === true;
7562
- if (!hasTx && !hasTenant && !isSystem) return base;
7564
+ if (!hasTx && !hasTenant && !isSystem && !hasTz) return base;
7563
7565
  const opts = base && typeof base === "object" ? { ...base } : {};
7564
7566
  if (hasTx && opts.transaction === void 0) {
7565
7567
  opts.transaction = tx;
@@ -7567,6 +7569,9 @@ var _ObjectQL = class _ObjectQL {
7567
7569
  if (hasTenant && opts.tenantId === void 0) {
7568
7570
  opts.tenantId = execCtx.tenantId;
7569
7571
  }
7572
+ if (hasTz && opts.timezone === void 0) {
7573
+ opts.timezone = execCtx.timezone;
7574
+ }
7570
7575
  if (isSystem && opts.bypassTenantAudit === void 0) {
7571
7576
  opts.bypassTenantAudit = true;
7572
7577
  }
@@ -7640,29 +7645,48 @@ var _ObjectQL = class _ObjectQL {
7640
7645
  * owns the value, not the client.
7641
7646
  *
7642
7647
  * In the fallback path the next value is `max(existing) + 1`, seeded once per
7643
- * `object.field` from the store then incremented in memory (monotonic within
7644
- * the process, resilient to deletions). `autonumberFormat` is honored, e.g.
7645
- * `CASE-{0000}` `CASE-0042`. NOTE: this in-memory seeding is single-instance.
7648
+ * `object.field.<scope>` from the store then incremented in memory (monotonic
7649
+ * within the process, resilient to deletions). The shared `autonumberFormat`
7650
+ * renderer is honored end-to-end, so date tokens (`AD{YYYYMMDD}{0000}`), field
7651
+ * interpolation (`{island_zone}{000}`) and per-scope reset behave identically
7652
+ * to the SQL driver's persistent sequence (#1603). NOTE: this in-memory seeding
7653
+ * is single-instance.
7646
7654
  */
7647
7655
  async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
7648
7656
  if (driverOwnsAutonumber) return;
7649
7657
  const fields = this.getSchema(object)?.fields;
7650
7658
  if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
7659
+ const now = /* @__PURE__ */ new Date();
7660
+ const timezone = execCtx?.timezone;
7651
7661
  for (const [name, def] of Object.entries(fields)) {
7652
7662
  if (def?.type !== "autonumber") continue;
7653
7663
  const current = record[name];
7654
7664
  if (current != null && current !== "") continue;
7655
- const key = `${object}.${name}`;
7656
- let next = this.autonumberCounters.get(key);
7657
- if (next == null) next = await this.seedAutonumber(object, name, execCtx);
7658
- next += 1;
7659
- this.autonumberCounters.set(key, next);
7660
7665
  const fmt = def.autonumberFormat ?? def.format;
7661
- record[name] = this.formatAutonumber(fmt, next);
7666
+ const tokens = parseAutonumberFormat(typeof fmt === "string" ? fmt : "");
7667
+ const missing = missingFieldValues(tokens, record);
7668
+ if (missing.length > 0) {
7669
+ throw new Error(
7670
+ `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.`
7671
+ );
7672
+ }
7673
+ const probe = renderAutonumber({ tokens, seq: 0, record, now, timezone });
7674
+ const counterKey = `${object}.${name}.${probe.scope}`;
7675
+ let next = this.autonumberCounters.get(counterKey);
7676
+ if (next == null) next = await this.seedAutonumber(object, name, probe.prefix, execCtx);
7677
+ next += 1;
7678
+ this.autonumberCounters.set(counterKey, next);
7679
+ record[name] = renderAutonumber({ tokens, seq: next, record, now, timezone }).value;
7662
7680
  }
7663
7681
  }
7664
- /** Seed the autonumber counter from the current max numeric value in store. */
7665
- async seedAutonumber(object, field, execCtx) {
7682
+ /**
7683
+ * Seed the autonumber counter from the current max in store, scoped to
7684
+ * `prefix`. With a non-empty prefix (date/field formats) only rows in the
7685
+ * same scope count, and the counter is the digit-run immediately after the
7686
+ * prefix; with an empty prefix (legacy fixed-prefix formats) the last digit
7687
+ * run of the whole value is used, preserving the original behaviour.
7688
+ */
7689
+ async seedAutonumber(object, field, prefix, execCtx) {
7666
7690
  try {
7667
7691
  const rows = await this.find(object, {
7668
7692
  select: ["id", field],
@@ -7673,22 +7697,24 @@ var _ObjectQL = class _ObjectQL {
7673
7697
  for (const r of rows || []) {
7674
7698
  const v = r?.[field];
7675
7699
  if (v == null) continue;
7676
- const m = String(v).match(/(\d+)(?!.*\d)/);
7677
- if (m) max = Math.max(max, parseInt(m[1], 10) || 0);
7700
+ const s = String(v);
7701
+ if (prefix && !s.startsWith(prefix)) continue;
7702
+ const tail = prefix ? s.slice(prefix.length) : s;
7703
+ let digits;
7704
+ if (prefix) {
7705
+ const head = tail.match(/^\d+/);
7706
+ digits = head ? head[0] : void 0;
7707
+ } else {
7708
+ const runs = tail.match(/\d+/g);
7709
+ digits = runs ? runs[runs.length - 1] : void 0;
7710
+ }
7711
+ if (digits) max = Math.max(max, parseInt(digits, 10) || 0);
7678
7712
  }
7679
7713
  return max;
7680
7714
  } catch {
7681
7715
  return 0;
7682
7716
  }
7683
7717
  }
7684
- /** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
7685
- formatAutonumber(format, value) {
7686
- if (!format) return String(value);
7687
- const m = format.match(/\{(0+)\}/);
7688
- if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
7689
- const padded = String(value).padStart(m[1].length, "0");
7690
- return format.replace(m[0], padded);
7691
- }
7692
7718
  /**
7693
7719
  * Register contribution (Manifest)
7694
7720
  *