@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 +62 -7
- package/dist/index.d.ts +62 -7
- package/dist/index.js +119 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +119 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -141,7 +141,10 @@ var init_seed_loader = __esm({
|
|
|
141
141
|
const seedIdentity = config.identity;
|
|
142
142
|
const baseEvalCtx = {
|
|
143
143
|
now: seedNow,
|
|
144
|
-
|
|
144
|
+
// `id: null` is a legitimate seed-time state (the owning admin does not
|
|
145
|
+
// exist yet) that the formula EvalContext's `user.id: string` type does
|
|
146
|
+
// not yet model — cast the fallback so `os.user.id` evaluates to null.
|
|
147
|
+
user: seedIdentity?.user ?? { id: null },
|
|
145
148
|
// Fall back to the per-tenant organizationId so `os.org.id` resolves
|
|
146
149
|
// during per-org replay even without an explicit identity.org.
|
|
147
150
|
org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
|
|
@@ -161,7 +164,7 @@ var init_seed_loader = __esm({
|
|
|
161
164
|
targetField: "(expression)",
|
|
162
165
|
attemptedValue: dataset.records[i],
|
|
163
166
|
recordIndex: i,
|
|
164
|
-
message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}.
|
|
167
|
+
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.`
|
|
165
168
|
};
|
|
166
169
|
errors.push(error);
|
|
167
170
|
allErrors.push(error);
|
|
@@ -840,6 +843,8 @@ function applySystemFields(schema, opts) {
|
|
|
840
843
|
const tenancyDisabled = schema.tenancy?.enabled === false;
|
|
841
844
|
const wantTenant = sf?.tenant !== false && !tenancyDisabled;
|
|
842
845
|
const wantAudit = sf?.audit !== false;
|
|
846
|
+
const ownership = schema.ownership;
|
|
847
|
+
const wantOwner = ownership !== "org" && ownership !== "none" && !schema.managedBy && !schema.name.startsWith("sys_");
|
|
843
848
|
const additions = {};
|
|
844
849
|
if (wantTenant && !schema.fields?.organization_id) {
|
|
845
850
|
additions.organization_id = {
|
|
@@ -898,6 +903,17 @@ function applySystemFields(schema, opts) {
|
|
|
898
903
|
};
|
|
899
904
|
}
|
|
900
905
|
}
|
|
906
|
+
if (wantOwner && !schema.fields?.owner_id) {
|
|
907
|
+
additions.owner_id = {
|
|
908
|
+
type: "lookup",
|
|
909
|
+
reference: "sys_user",
|
|
910
|
+
label: "Owner",
|
|
911
|
+
required: false,
|
|
912
|
+
readonly: false,
|
|
913
|
+
system: true,
|
|
914
|
+
description: "Record owner (auto-stamped to the creating user on insert; reassignable). Drives owner-scoped views, reports and notifications."
|
|
915
|
+
};
|
|
916
|
+
}
|
|
901
917
|
if (Object.keys(additions).length === 0) return schema;
|
|
902
918
|
return {
|
|
903
919
|
...schema,
|
|
@@ -6117,6 +6133,7 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
|
6117
6133
|
|
|
6118
6134
|
// src/engine.ts
|
|
6119
6135
|
import { AsyncLocalStorage } from "async_hooks";
|
|
6136
|
+
import { parseAutonumberFormat, renderAutonumber, missingFieldValues } from "@objectstack/spec/data";
|
|
6120
6137
|
import { ExecutionContextSchema } from "@objectstack/spec/kernel";
|
|
6121
6138
|
import { createLogger } from "@objectstack/core";
|
|
6122
6139
|
import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
|
|
@@ -7025,7 +7042,7 @@ function aggregateBucket(rows, aggregations) {
|
|
|
7025
7042
|
const alias = agg.alias;
|
|
7026
7043
|
const fn = agg.function;
|
|
7027
7044
|
if (fn === "count") {
|
|
7028
|
-
if (!agg.field) {
|
|
7045
|
+
if (!agg.field || agg.field === "*") {
|
|
7029
7046
|
out[alias] = rows.length;
|
|
7030
7047
|
} else {
|
|
7031
7048
|
out[alias] = rows.reduce(
|
|
@@ -7542,8 +7559,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7542
7559
|
const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
|
|
7543
7560
|
const hasTx = tx !== void 0;
|
|
7544
7561
|
const hasTenant = execCtx?.tenantId !== void 0;
|
|
7562
|
+
const hasTz = execCtx?.timezone !== void 0;
|
|
7545
7563
|
const isSystem = execCtx?.isSystem === true;
|
|
7546
|
-
if (!hasTx && !hasTenant && !isSystem) return base;
|
|
7564
|
+
if (!hasTx && !hasTenant && !isSystem && !hasTz) return base;
|
|
7547
7565
|
const opts = base && typeof base === "object" ? { ...base } : {};
|
|
7548
7566
|
if (hasTx && opts.transaction === void 0) {
|
|
7549
7567
|
opts.transaction = tx;
|
|
@@ -7551,6 +7569,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7551
7569
|
if (hasTenant && opts.tenantId === void 0) {
|
|
7552
7570
|
opts.tenantId = execCtx.tenantId;
|
|
7553
7571
|
}
|
|
7572
|
+
if (hasTz && opts.timezone === void 0) {
|
|
7573
|
+
opts.timezone = execCtx.timezone;
|
|
7574
|
+
}
|
|
7554
7575
|
if (isSystem && opts.bypassTenantAudit === void 0) {
|
|
7555
7576
|
opts.bypassTenantAudit = true;
|
|
7556
7577
|
}
|
|
@@ -7624,29 +7645,48 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7624
7645
|
* owns the value, not the client.
|
|
7625
7646
|
*
|
|
7626
7647
|
* In the fallback path the next value is `max(existing) + 1`, seeded once per
|
|
7627
|
-
* `object.field
|
|
7628
|
-
* the process, resilient to deletions). `autonumberFormat`
|
|
7629
|
-
*
|
|
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.
|
|
7630
7654
|
*/
|
|
7631
7655
|
async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
|
|
7632
7656
|
if (driverOwnsAutonumber) return;
|
|
7633
7657
|
const fields = this.getSchema(object)?.fields;
|
|
7634
7658
|
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
|
|
7659
|
+
const now = /* @__PURE__ */ new Date();
|
|
7660
|
+
const timezone = execCtx?.timezone;
|
|
7635
7661
|
for (const [name, def] of Object.entries(fields)) {
|
|
7636
7662
|
if (def?.type !== "autonumber") continue;
|
|
7637
7663
|
const current = record[name];
|
|
7638
7664
|
if (current != null && current !== "") continue;
|
|
7639
|
-
const key = `${object}.${name}`;
|
|
7640
|
-
let next = this.autonumberCounters.get(key);
|
|
7641
|
-
if (next == null) next = await this.seedAutonumber(object, name, execCtx);
|
|
7642
|
-
next += 1;
|
|
7643
|
-
this.autonumberCounters.set(key, next);
|
|
7644
7665
|
const fmt = def.autonumberFormat ?? def.format;
|
|
7645
|
-
|
|
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;
|
|
7646
7680
|
}
|
|
7647
7681
|
}
|
|
7648
|
-
/**
|
|
7649
|
-
|
|
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) {
|
|
7650
7690
|
try {
|
|
7651
7691
|
const rows = await this.find(object, {
|
|
7652
7692
|
select: ["id", field],
|
|
@@ -7657,22 +7697,24 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7657
7697
|
for (const r of rows || []) {
|
|
7658
7698
|
const v = r?.[field];
|
|
7659
7699
|
if (v == null) continue;
|
|
7660
|
-
const
|
|
7661
|
-
if (
|
|
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);
|
|
7662
7712
|
}
|
|
7663
7713
|
return max;
|
|
7664
7714
|
} catch {
|
|
7665
7715
|
return 0;
|
|
7666
7716
|
}
|
|
7667
7717
|
}
|
|
7668
|
-
/** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
|
|
7669
|
-
formatAutonumber(format, value) {
|
|
7670
|
-
if (!format) return String(value);
|
|
7671
|
-
const m = format.match(/\{(0+)\}/);
|
|
7672
|
-
if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
|
|
7673
|
-
const padded = String(value).padStart(m[1].length, "0");
|
|
7674
|
-
return format.replace(m[0], padded);
|
|
7675
|
-
}
|
|
7676
7718
|
/**
|
|
7677
7719
|
* Register contribution (Manifest)
|
|
7678
7720
|
*
|
|
@@ -9448,6 +9490,58 @@ var ScopedContext = class _ScopedContext {
|
|
|
9448
9490
|
throw error;
|
|
9449
9491
|
}
|
|
9450
9492
|
}
|
|
9493
|
+
/**
|
|
9494
|
+
* Resolve the default driver, if it exposes transaction primitives.
|
|
9495
|
+
* Shared by {@link transaction} and the discrete begin/commit/rollback trio.
|
|
9496
|
+
*/
|
|
9497
|
+
txDriver() {
|
|
9498
|
+
const engine = this.engine;
|
|
9499
|
+
const driver = engine.defaultDriver ? engine.drivers?.get(engine.defaultDriver) : void 0;
|
|
9500
|
+
return driver?.beginTransaction ? driver : void 0;
|
|
9501
|
+
}
|
|
9502
|
+
/**
|
|
9503
|
+
* Discrete transaction primitives — `begin` / `commit` / `rollback` as three
|
|
9504
|
+
* separate calls, in contrast to {@link transaction}'s single-callback form.
|
|
9505
|
+
*
|
|
9506
|
+
* This trio exists for callers that cannot keep a JS closure on the stack for
|
|
9507
|
+
* the lifetime of the transaction — chiefly the sandbox runner, where the
|
|
9508
|
+
* hook/action body's `ctx.api.transaction(fn)` is driven across many host
|
|
9509
|
+
* event-loop turns via deferred promises. Across those `setImmediate`
|
|
9510
|
+
* boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
|
|
9511
|
+
* survive, so the transaction handle is threaded **explicitly**: `begin`
|
|
9512
|
+
* returns a child ScopedContext carrying `transaction: trx` in its execution
|
|
9513
|
+
* context, and `resolveTx` honors that explicit handle ahead of the ambient
|
|
9514
|
+
* store. Every `object(...)` op on the returned context therefore reuses the
|
|
9515
|
+
* one connection without relying on ALS.
|
|
9516
|
+
*
|
|
9517
|
+
* Returns `null` when the driver has no transaction support — the caller then
|
|
9518
|
+
* runs non-transactionally against `this` (same graceful degrade as
|
|
9519
|
+
* {@link transaction}).
|
|
9520
|
+
*/
|
|
9521
|
+
async beginTransaction() {
|
|
9522
|
+
const driver = this.txDriver();
|
|
9523
|
+
if (!driver) return null;
|
|
9524
|
+
const trx = await driver.beginTransaction();
|
|
9525
|
+
const ctx = new _ScopedContext(
|
|
9526
|
+
{ ...this.executionContext, transaction: trx },
|
|
9527
|
+
this.engine
|
|
9528
|
+
);
|
|
9529
|
+
return { ctx, handle: trx };
|
|
9530
|
+
}
|
|
9531
|
+
/** Commit a handle obtained from {@link beginTransaction}. */
|
|
9532
|
+
async commitTransaction(handle) {
|
|
9533
|
+
const driver = this.txDriver();
|
|
9534
|
+
if (!driver) return;
|
|
9535
|
+
if (driver.commit) await driver.commit(handle);
|
|
9536
|
+
else if (driver.commitTransaction) await driver.commitTransaction(handle);
|
|
9537
|
+
}
|
|
9538
|
+
/** Roll back a handle obtained from {@link beginTransaction}. */
|
|
9539
|
+
async rollbackTransaction(handle) {
|
|
9540
|
+
const driver = this.txDriver();
|
|
9541
|
+
if (!driver) return;
|
|
9542
|
+
if (driver.rollback) await driver.rollback(handle);
|
|
9543
|
+
else if (driver.rollbackTransaction) await driver.rollbackTransaction(handle);
|
|
9544
|
+
}
|
|
9451
9545
|
get userId() {
|
|
9452
9546
|
return this.executionContext.userId;
|
|
9453
9547
|
}
|