Skip to content

Commit 2b01395

Browse files
committed
refactor(objectql): move the caller-envelope builders out of engine.ts into engine-execution-plumbing.ts (#25522 cut 9)
Move-only. The bodies of buildSession, buildProvenance, buildEvalUser and buildUser move unedited (re-indented one level) into packages/objectql/src/engine-execution-plumbing.ts. None of them reads an engine member, so each method keeps its name, modifiers, signature, docblock and position and calls the function of the same name directly. The system-context census page re-keys row 63 (the isSystem carry in buildSession) to the new module, where the read site now lives. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYrEgnrA1u8hdMTsRxqJ4L
1 parent 1eebbc0 commit 2b01395

3 files changed

Lines changed: 134 additions & 67 deletions

File tree

‎content/docs/permissions/system-context.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ a reader tracing where elevation travels needs them.
210210

211211
| # | Site | Package | What it does |
212212
|:--|:---|:---|:---|
213-
| 63 | `packages/objectql/src/engine.ts#buildSession` | objectql | Propagates `isSystem` into the hook session so hooks can tell engine self-writes from user writes |
213+
| 63 | `packages/objectql/src/engine-execution-plumbing.ts#buildSession` | objectql | Propagates `isSystem` into the hook session so hooks can tell engine self-writes from user writes |
214214
| 64 | `packages/objectql/src/engine.ts#isSystem` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag |
215215
| 65 | `packages/runtime/src/sandbox/body-runner.ts#executionContextFromHook` | runtime | Rebuilds an `ExecutionContext` from a hook session, carrying the flag across |
216216

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25522 cut 9] The caller-envelope builders of `ObjectQL`'s execution
5+
* plumbing, moved out of `engine.ts` whole: `buildSession` (the
6+
* `HookContext.session` a hook reads, WHO is calling), `buildProvenance` (the
7+
* `HookContext.provenance` envelope, WHERE a write came from), `buildEvalUser`
8+
* (the ADR-0068 `current_user` validation-time predicates read) and
9+
* `buildUser` (the `HookContext.user` shortcut).
10+
*
11+
* A MOVE, not a redesign. Each exported function below is the body of the
12+
* `ObjectQL` method of the same name as it stood, re-indented one level and
13+
* otherwise unedited: the same copy list, the same flags carried only when
14+
* set, and the same `undefined` for a context with nothing to say. Each
15+
* method's docblock stays on the method in `engine.ts`, which is still the
16+
* contract.
17+
*
18+
* What stays in the class: `executeWithMiddleware`, because ADR-0139 cites it
19+
* by `engine.ts#executeWithMiddleware`; `buildReferentialFieldClear` and
20+
* `claimReferentialCascadeDelete`, whose bodies are no longer than the call
21+
* (and, for the second, the host literal) moving them would cost.
22+
*
23+
* ## How the moved bodies reach the class
24+
*
25+
* None of the four reads a member of the engine: each is a function of the
26+
* `ExecutionContext` it is handed. So there is no host object here — there
27+
* is no shared state to hand over — and each method keeps its name,
28+
* modifiers, signature, docblock and position and calls the function of the
29+
* same name directly, as the member-free bodies of earlier cuts do
30+
* (`engine-validation-messages.ts#objectDisplayLabel`).
31+
*
32+
* This module imports nothing from the engine, so there is no module cycle at
33+
* run time, and nothing re-exports it, so the published surface does not
34+
* move.
35+
*/
36+
37+
import type { HookContext } from '@objectstack/spec/data';
38+
import type { ExecutionContext } from '@objectstack/spec/kernel';
39+
40+
/** The body of `ObjectQL#buildSession`; the method's docblock there is its contract. */
41+
export function buildSession(execCtx?: ExecutionContext): HookContext['session'] {
42+
if (!execCtx) return undefined;
43+
const session = {
44+
userId: execCtx.userId,
45+
// `organizationId` is the blessed developer-facing name for the caller's
46+
// active org (matches the `organization_id` column, `current_user`
47+
// RLS shape, and seed rows). It comes from `execCtx.tenantId`, which the
48+
// kernel resolves from `session.activeOrganizationId`. The deprecated
49+
// `session.tenantId` alias (#3280) was removed here in v16 (#3290) — the
50+
// driver-layer `execCtx.tenantId` knob is a separate axis and stays.
51+
organizationId: execCtx.tenantId,
52+
positions: execCtx.positions,
53+
accessToken: execCtx.accessToken,
54+
// Propagate system-elevated flag so hooks can distinguish engine
55+
// self-writes (e.g. approval status mirror) from genuine user writes.
56+
...((execCtx as any).isSystem ? { isSystem: true } : {}),
57+
// Propagate the service-principal label (`ExecutionContext.actor`,
58+
// e.g. `svc:flow:<name>`) so a non-user write stays attributable in the
59+
// audit log — the writer's `userId ?? session.actor` fallback is dead
60+
// without this hop (the `ExecutionContext.actor` audit-attribution
61+
// contract, `@objectstack/spec` kernel/execution-context.zod.ts; #4366).
62+
...(typeof (execCtx as any).actor === 'string' && (execCtx as any).actor
63+
? { actor: (execCtx as any).actor }
64+
: {}),
65+
// Propagate the automation-suppression flag so the record-change trigger
66+
// can skip flow dispatch for seed/bulk writes (ADR: seed loads end-state
67+
// data, not user events). `skipAutomations` implies `skipTriggers` —
68+
// suppressing metadata hooks while still dispatching flows would leave
69+
// the "run automations & triggers" opt-out half-working (#2922).
70+
...((execCtx as any).skipTriggers || (execCtx as any).skipAutomations ? { skipTriggers: true } : {}),
71+
// Propagate the full automation opt-out so `triggerHooks` can skip
72+
// metadata-bound hooks (import with "run automations" unchecked, undo).
73+
...((execCtx as any).skipAutomations ? { skipAutomations: true } : {}),
74+
// Propagate the historical-import audit-preservation flag so the built-in
75+
// audit hook keeps a client-supplied updated_at/updated_by instead of
76+
// stamping now (#3493). Opt-in, server-set only.
77+
...((execCtx as any).preserveAudit ? { preserveAudit: true } : {}),
78+
} as HookContext['session'];
79+
// Nothing to say about the caller → say nothing. An object whose every
80+
// property is `undefined` conveys no more than no session at all, and
81+
// returning it would turn "no caller" into "an anonymous caller".
82+
return Object.values(session as Record<string, unknown>).some((v) => v !== undefined)
83+
? session
84+
: undefined;
85+
}
86+
87+
/** The body of `ObjectQL#buildProvenance`; the method's docblock there is its contract. */
88+
export function buildProvenance(execCtx?: ExecutionContext): HookContext['provenance'] {
89+
const flowRunId = (execCtx as any)?.flowRunId;
90+
const attributedUserId = (execCtx as any)?.attributedUserId;
91+
const performedByClientId = (execCtx as any)?.performedBy?.clientId;
92+
if (!flowRunId && !attributedUserId && !performedByClientId) return undefined;
93+
return {
94+
...(flowRunId ? { flowRunId: String(flowRunId) } : {}),
95+
...(attributedUserId ? { attributedUserId: String(attributedUserId) } : {}),
96+
...(performedByClientId ? { performedByClientId: String(performedByClientId) } : {}),
97+
};
98+
}
99+
100+
/** The body of `ObjectQL#buildEvalUser`; the method's docblock there is its contract. */
101+
export function buildEvalUser(
102+
execCtx?: ExecutionContext,
103+
): { id: string; positions: string[]; organizationId: string | null } | undefined {
104+
if (!execCtx || execCtx.userId == null) return undefined;
105+
return {
106+
id: String(execCtx.userId),
107+
positions: execCtx.positions ?? [],
108+
organizationId: execCtx.tenantId != null ? String(execCtx.tenantId) : null,
109+
};
110+
}
111+
112+
/** The body of `ObjectQL#buildUser`; the method's docblock there is its contract. */
113+
export function buildUser(execCtx?: ExecutionContext): HookContext['user'] {
114+
if (!execCtx || execCtx.userId == null) return undefined;
115+
return {
116+
id: String(execCtx.userId),
117+
...(execCtx.email ? { email: execCtx.email } : {}),
118+
// Always equals `session.organizationId` (both from `execCtx.tenantId`).
119+
// Undefined on unscoped (platform/community) calls.
120+
...(execCtx.tenantId != null ? { organizationId: String(execCtx.tenantId) } : {}),
121+
};
122+
}

‎packages/objectql/src/engine.ts‎

Lines changed: 11 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ import { enforceTransactionOrigin, execute, warnTransactionUnsupported } from '.
139139
// [#25522 cut 8] The compatibility-API bodies; the methods of the same names
140140
// below hand each body the members it reads (`CompatibilityApiHost`).
141141
import { getConfigs, introspectDatasource, registerObject, removePackage, syncObjectSchema, syncSchemas } from './engine-compatibility-api.js';
142+
// [#25522 cut 9] The hook-context caller-envelope builders; the methods of the
143+
// same names below call them, and none of the bodies reads a member.
144+
import { buildEvalUser, buildProvenance, buildSession, buildUser } from './engine-execution-plumbing.js';
142145
import { ExecutionContext, ExecutionContextSchema, type InstalledPackageSource } from '@objectstack/spec/kernel';
143146
import { isStoredMetadataBodyObject } from '@objectstack/spec/kernel';
144147
import type { FlowFunctionEffect } from '@objectstack/spec/automation';
@@ -7077,49 +7080,8 @@ export class ObjectQL implements IObjectQLEngine {
70777080
* session and stays gated.
70787081
*/
70797082
private buildSession(execCtx?: ExecutionContext): HookContext['session'] {
7080-
if (!execCtx) return undefined;
7081-
const session = {
7082-
userId: execCtx.userId,
7083-
// `organizationId` is the blessed developer-facing name for the caller's
7084-
// active org (matches the `organization_id` column, `current_user`
7085-
// RLS shape, and seed rows). It comes from `execCtx.tenantId`, which the
7086-
// kernel resolves from `session.activeOrganizationId`. The deprecated
7087-
// `session.tenantId` alias (#3280) was removed here in v16 (#3290) — the
7088-
// driver-layer `execCtx.tenantId` knob is a separate axis and stays.
7089-
organizationId: execCtx.tenantId,
7090-
positions: execCtx.positions,
7091-
accessToken: execCtx.accessToken,
7092-
// Propagate system-elevated flag so hooks can distinguish engine
7093-
// self-writes (e.g. approval status mirror) from genuine user writes.
7094-
...((execCtx as any).isSystem ? { isSystem: true } : {}),
7095-
// Propagate the service-principal label (`ExecutionContext.actor`,
7096-
// e.g. `svc:flow:<name>`) so a non-user write stays attributable in the
7097-
// audit log — the writer's `userId ?? session.actor` fallback is dead
7098-
// without this hop (the `ExecutionContext.actor` audit-attribution
7099-
// contract, `@objectstack/spec` kernel/execution-context.zod.ts; #4366).
7100-
...(typeof (execCtx as any).actor === 'string' && (execCtx as any).actor
7101-
? { actor: (execCtx as any).actor }
7102-
: {}),
7103-
// Propagate the automation-suppression flag so the record-change trigger
7104-
// can skip flow dispatch for seed/bulk writes (ADR: seed loads end-state
7105-
// data, not user events). `skipAutomations` implies `skipTriggers` —
7106-
// suppressing metadata hooks while still dispatching flows would leave
7107-
// the "run automations & triggers" opt-out half-working (#2922).
7108-
...((execCtx as any).skipTriggers || (execCtx as any).skipAutomations ? { skipTriggers: true } : {}),
7109-
// Propagate the full automation opt-out so `triggerHooks` can skip
7110-
// metadata-bound hooks (import with "run automations" unchecked, undo).
7111-
...((execCtx as any).skipAutomations ? { skipAutomations: true } : {}),
7112-
// Propagate the historical-import audit-preservation flag so the built-in
7113-
// audit hook keeps a client-supplied updated_at/updated_by instead of
7114-
// stamping now (#3493). Opt-in, server-set only.
7115-
...((execCtx as any).preserveAudit ? { preserveAudit: true } : {}),
7116-
} as HookContext['session'];
7117-
// Nothing to say about the caller → say nothing. An object whose every
7118-
// property is `undefined` conveys no more than no session at all, and
7119-
// returning it would turn "no caller" into "an anonymous caller".
7120-
return Object.values(session as Record<string, unknown>).some((v) => v !== undefined)
7121-
? session
7122-
: undefined;
7083+
// [#25522 cut 9] The body moved to `engine-execution-plumbing.ts`; it reads no member.
7084+
return buildSession(execCtx);
71237085
}
71247086

71257087
/**
@@ -7149,15 +7111,8 @@ export class ObjectQL implements IObjectQLEngine {
71497111
* populated (ADR-0049).
71507112
*/
71517113
private buildProvenance(execCtx?: ExecutionContext): HookContext['provenance'] {
7152-
const flowRunId = (execCtx as any)?.flowRunId;
7153-
const attributedUserId = (execCtx as any)?.attributedUserId;
7154-
const performedByClientId = (execCtx as any)?.performedBy?.clientId;
7155-
if (!flowRunId && !attributedUserId && !performedByClientId) return undefined;
7156-
return {
7157-
...(flowRunId ? { flowRunId: String(flowRunId) } : {}),
7158-
...(attributedUserId ? { attributedUserId: String(attributedUserId) } : {}),
7159-
...(performedByClientId ? { performedByClientId: String(performedByClientId) } : {}),
7160-
};
7114+
// [#25522 cut 9] The body moved to `engine-execution-plumbing.ts`; it reads no member.
7115+
return buildProvenance(execCtx);
71617116
}
71627117

71637118
/**
@@ -7217,12 +7172,8 @@ export class ObjectQL implements IObjectQLEngine {
72177172
private buildEvalUser(
72187173
execCtx?: ExecutionContext,
72197174
): { id: string; positions: string[]; organizationId: string | null } | undefined {
7220-
if (!execCtx || execCtx.userId == null) return undefined;
7221-
return {
7222-
id: String(execCtx.userId),
7223-
positions: execCtx.positions ?? [],
7224-
organizationId: execCtx.tenantId != null ? String(execCtx.tenantId) : null,
7225-
};
7175+
// [#25522 cut 9] The body moved to `engine-execution-plumbing.ts`; it reads no member.
7176+
return buildEvalUser(execCtx);
72267177
}
72277178

72287179
/**
@@ -7237,14 +7188,8 @@ export class ObjectQL implements IObjectQLEngine {
72377188
* `ctx.session.organizationId`, which is populated whenever a session is.
72387189
*/
72397190
private buildUser(execCtx?: ExecutionContext): HookContext['user'] {
7240-
if (!execCtx || execCtx.userId == null) return undefined;
7241-
return {
7242-
id: String(execCtx.userId),
7243-
...(execCtx.email ? { email: execCtx.email } : {}),
7244-
// Always equals `session.organizationId` (both from `execCtx.tenantId`).
7245-
// Undefined on unscoped (platform/community) calls.
7246-
...(execCtx.tenantId != null ? { organizationId: String(execCtx.tenantId) } : {}),
7247-
};
7191+
// [#25522 cut 9] The body moved to `engine-execution-plumbing.ts`; it reads no member.
7192+
return buildUser(execCtx);
72487193
}
72497194

72507195
/**

0 commit comments

Comments
 (0)