@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.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
|
|
2213
|
-
* the process, resilient to deletions). `autonumberFormat`
|
|
2214
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
2213
|
-
* the process, resilient to deletions). `autonumberFormat`
|
|
2214
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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}.
|
|
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
|
|
7687
|
-
* the process, resilient to deletions). `autonumberFormat`
|
|
7688
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
7708
|
-
|
|
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
|
|
7720
|
-
if (
|
|
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
|
}
|