@objectstack/objectql 9.9.1 → 9.10.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;
@@ -2664,6 +2680,38 @@ declare class ScopedContext {
2664
2680
  * does not support transactions.
2665
2681
  */
2666
2682
  transaction(callback: (trxCtx: ScopedContext) => Promise<any>): Promise<any>;
2683
+ /**
2684
+ * Resolve the default driver, if it exposes transaction primitives.
2685
+ * Shared by {@link transaction} and the discrete begin/commit/rollback trio.
2686
+ */
2687
+ private txDriver;
2688
+ /**
2689
+ * Discrete transaction primitives — `begin` / `commit` / `rollback` as three
2690
+ * separate calls, in contrast to {@link transaction}'s single-callback form.
2691
+ *
2692
+ * This trio exists for callers that cannot keep a JS closure on the stack for
2693
+ * the lifetime of the transaction — chiefly the sandbox runner, where the
2694
+ * hook/action body's `ctx.api.transaction(fn)` is driven across many host
2695
+ * event-loop turns via deferred promises. Across those `setImmediate`
2696
+ * boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
2697
+ * survive, so the transaction handle is threaded **explicitly**: `begin`
2698
+ * returns a child ScopedContext carrying `transaction: trx` in its execution
2699
+ * context, and `resolveTx` honors that explicit handle ahead of the ambient
2700
+ * store. Every `object(...)` op on the returned context therefore reuses the
2701
+ * one connection without relying on ALS.
2702
+ *
2703
+ * Returns `null` when the driver has no transaction support — the caller then
2704
+ * runs non-transactionally against `this` (same graceful degrade as
2705
+ * {@link transaction}).
2706
+ */
2707
+ beginTransaction(): Promise<{
2708
+ ctx: ScopedContext;
2709
+ handle: unknown;
2710
+ } | null>;
2711
+ /** Commit a handle obtained from {@link beginTransaction}. */
2712
+ commitTransaction(handle: unknown): Promise<void>;
2713
+ /** Roll back a handle obtained from {@link beginTransaction}. */
2714
+ rollbackTransaction(handle: unknown): Promise<void>;
2667
2715
  get userId(): string | undefined;
2668
2716
  get tenantId(): string | undefined;
2669
2717
  /** 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;
@@ -2664,6 +2680,38 @@ declare class ScopedContext {
2664
2680
  * does not support transactions.
2665
2681
  */
2666
2682
  transaction(callback: (trxCtx: ScopedContext) => Promise<any>): Promise<any>;
2683
+ /**
2684
+ * Resolve the default driver, if it exposes transaction primitives.
2685
+ * Shared by {@link transaction} and the discrete begin/commit/rollback trio.
2686
+ */
2687
+ private txDriver;
2688
+ /**
2689
+ * Discrete transaction primitives — `begin` / `commit` / `rollback` as three
2690
+ * separate calls, in contrast to {@link transaction}'s single-callback form.
2691
+ *
2692
+ * This trio exists for callers that cannot keep a JS closure on the stack for
2693
+ * the lifetime of the transaction — chiefly the sandbox runner, where the
2694
+ * hook/action body's `ctx.api.transaction(fn)` is driven across many host
2695
+ * event-loop turns via deferred promises. Across those `setImmediate`
2696
+ * boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
2697
+ * survive, so the transaction handle is threaded **explicitly**: `begin`
2698
+ * returns a child ScopedContext carrying `transaction: trx` in its execution
2699
+ * context, and `resolveTx` honors that explicit handle ahead of the ambient
2700
+ * store. Every `object(...)` op on the returned context therefore reuses the
2701
+ * one connection without relying on ALS.
2702
+ *
2703
+ * Returns `null` when the driver has no transaction support — the caller then
2704
+ * runs non-transactionally against `this` (same graceful degrade as
2705
+ * {@link transaction}).
2706
+ */
2707
+ beginTransaction(): Promise<{
2708
+ ctx: ScopedContext;
2709
+ handle: unknown;
2710
+ } | null>;
2711
+ /** Commit a handle obtained from {@link beginTransaction}. */
2712
+ commitTransaction(handle: unknown): Promise<void>;
2713
+ /** Roll back a handle obtained from {@link beginTransaction}. */
2714
+ rollbackTransaction(handle: unknown): Promise<void>;
2667
2715
  get userId(): string | undefined;
2668
2716
  get tenantId(): string | undefined;
2669
2717
  /** 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,
@@ -7084,7 +7100,7 @@ function aggregateBucket(rows, aggregations) {
7084
7100
  const alias = agg.alias;
7085
7101
  const fn = agg.function;
7086
7102
  if (fn === "count") {
7087
- if (!agg.field) {
7103
+ if (!agg.field || agg.field === "*") {
7088
7104
  out[alias] = rows.length;
7089
7105
  } else {
7090
7106
  out[alias] = rows.reduce(
@@ -9507,6 +9523,58 @@ var ScopedContext = class _ScopedContext {
9507
9523
  throw error;
9508
9524
  }
9509
9525
  }
9526
+ /**
9527
+ * Resolve the default driver, if it exposes transaction primitives.
9528
+ * Shared by {@link transaction} and the discrete begin/commit/rollback trio.
9529
+ */
9530
+ txDriver() {
9531
+ const engine = this.engine;
9532
+ const driver = engine.defaultDriver ? engine.drivers?.get(engine.defaultDriver) : void 0;
9533
+ return driver?.beginTransaction ? driver : void 0;
9534
+ }
9535
+ /**
9536
+ * Discrete transaction primitives — `begin` / `commit` / `rollback` as three
9537
+ * separate calls, in contrast to {@link transaction}'s single-callback form.
9538
+ *
9539
+ * This trio exists for callers that cannot keep a JS closure on the stack for
9540
+ * the lifetime of the transaction — chiefly the sandbox runner, where the
9541
+ * hook/action body's `ctx.api.transaction(fn)` is driven across many host
9542
+ * event-loop turns via deferred promises. Across those `setImmediate`
9543
+ * boundaries the engine's ambient `txStore` (AsyncLocalStorage) does NOT
9544
+ * survive, so the transaction handle is threaded **explicitly**: `begin`
9545
+ * returns a child ScopedContext carrying `transaction: trx` in its execution
9546
+ * context, and `resolveTx` honors that explicit handle ahead of the ambient
9547
+ * store. Every `object(...)` op on the returned context therefore reuses the
9548
+ * one connection without relying on ALS.
9549
+ *
9550
+ * Returns `null` when the driver has no transaction support — the caller then
9551
+ * runs non-transactionally against `this` (same graceful degrade as
9552
+ * {@link transaction}).
9553
+ */
9554
+ async beginTransaction() {
9555
+ const driver = this.txDriver();
9556
+ if (!driver) return null;
9557
+ const trx = await driver.beginTransaction();
9558
+ const ctx = new _ScopedContext(
9559
+ { ...this.executionContext, transaction: trx },
9560
+ this.engine
9561
+ );
9562
+ return { ctx, handle: trx };
9563
+ }
9564
+ /** Commit a handle obtained from {@link beginTransaction}. */
9565
+ async commitTransaction(handle) {
9566
+ const driver = this.txDriver();
9567
+ if (!driver) return;
9568
+ if (driver.commit) await driver.commit(handle);
9569
+ else if (driver.commitTransaction) await driver.commitTransaction(handle);
9570
+ }
9571
+ /** Roll back a handle obtained from {@link beginTransaction}. */
9572
+ async rollbackTransaction(handle) {
9573
+ const driver = this.txDriver();
9574
+ if (!driver) return;
9575
+ if (driver.rollback) await driver.rollback(handle);
9576
+ else if (driver.rollbackTransaction) await driver.rollbackTransaction(handle);
9577
+ }
9510
9578
  get userId() {
9511
9579
  return this.executionContext.userId;
9512
9580
  }