Skip to content

Commit 6fbe981

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22743-field-types-currency-returntype
2 parents 7237ed7 + a8f24b0 commit 6fbe981

7 files changed

Lines changed: 798 additions & 9 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/verify': minor
3+
---
4+
5+
feat(verify): the handle gains a door that observes the writes the engine receives, a hook's refused write included
6+
7+
Clause-②: yes (widening)
8+
9+
An app's tests can see what a hook handed the engine through the in-process handle, so a suite no longer wraps the booted engine's write methods in a spy to find a refused write, to wait for a fire-and-forget hook, or to stage a refusal.
10+
11+
- **`stack.observeWrites(fn)`** runs `fn` and resolves with every insert, update and delete the booted engine received meanwhile, in the order each reached it. Each entry (`ObservedWrite`) carries `object`, `operation`, the payload (`data`), the update or delete selection (`where`), `multi`, the execution context the write carried (`context`), and the engine's answer (`outcome`): `pending`, then `resolved` with its result, or `refused` with the error the engine threw, unchanged, and that error's own `code`.
12+
- **What it is.** One global middleware on the engine's own chain (`registerMiddleware`), added on the first call. It passes every operation on unchanged and decides nothing. So it sits after the write gates the boot registered. Every write those gates let through is recorded, from any door (`hooks.run`, `seed`, a flow, a route, a hook's `ctx.api`), and so is every refusal the engine raises past them: a declared validation rule (`VALIDATION_FAILED`), a hook's throw, a unique conflict (`DUPLICATE_RECORD`). A write a gate refuses, the permission check among them, never reaches it and leaves no entry. Its refusal still reaches whoever made the write.
13+
- **An outcome is the engine's answer to that call, not a statement about stored rows.** A write an `after*` hook refuses was refused after the driver wrote, and its row can stay. Read rows back with `stack.rows`.
14+
- **The window is time, not cause.** A write the stack makes for its own reasons while `fn` runs is recorded too, so filter by `object`.
15+
- **An `async: true` hook.** Its write reaches the engine after the write that fired it has returned. `fn` is handed the live observation (`WriteObservation`), and `observation.settled(match)` resolves with the first write `match` accepts once the engine has answered it. It rejects after `timeoutMs` (default 2000), naming every write observed, or when the observation closes first. A hook that writes nothing leaves nothing to observe.
16+
- **Staging a refusal.** Arrange one the engine makes itself: seed the row that already holds a unique value the hook will write, or set up the state a declared validation rule refuses. The handle has no switch that refuses a write in the engine's place.
17+
- A `fn` that is not a function, a `match` that is not a function, and a `timeoutMs` that is not a positive number are refused with `code: 'INVALID_REQUEST'`, `status: 400`. A rejection of `fn` is rethrown unchanged once the observation has closed. No error code is added.
18+
- New exported types: `ObservedWrite`, `ObservedWriteOutcome`, `WriteObservation`.

‎packages/qa/dogfood/test/rls-runner.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ function fakeStack(opts: FakeOpts): VerifyStack {
136136
automation: undefined as never,
137137
seed: undefined as never,
138138
rows: undefined as never,
139+
observeWrites: undefined as never,
139140
metadata: undefined as never,
140141
tenancy: undefined as never,
141142
};

‎packages/verify/README.md‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ await stack.stop();
7676

7777
Every `VerifyStack` carries it; nothing extra to boot. Each method is a thin
7878
facade over a door the kernel wired at boot — the ObjectQL engine's own write,
79-
dry-run and read calls, the runtime's `/automation` and `/actions` routes
80-
driven in-process, the automation engine's condition evaluator, the
81-
`SchemaRegistry`, the `tenancy` service — with **zero
79+
dry-run and read calls and its middleware chain, the runtime's `/automation`
80+
and `/actions` routes driven in-process, the automation engine's condition
81+
evaluator, the `SchemaRegistry`, the `tenancy` service — with **zero
8282
re-implemented semantics**: the handle assembles no execution context, orders
8383
no hooks, evaluates no permission. What the engine does is what you assert on.
8484

@@ -137,6 +137,16 @@ expect(await stack.automation.evaluateCondition(
137137
const [acc] = await stack.seed('crm_account', [{ name: 'Globex' }]);
138138
const mine = await stack.rows('crm_opportunity', { crm_account: acc.id }, { as: rep });
139139

140+
// What a hook handed the engine: every write the engine receives while the
141+
// function runs, and what it answered. A hook's write the engine refused
142+
// leaves no row; here it is, refused, with the engine's own error.
143+
const writes = await stack.observeWrites(async (observation) => {
144+
await stack.hooks.run('crm_lead', 'insert', { last_name: 'Ada', company: 'Globex' }, { as: rep });
145+
// An `async: true` hook writes after the triggering write has returned.
146+
await observation.settled((w) => w.object === 'crm_task');
147+
});
148+
const refused = writes.filter((w) => w.outcome.status === 'refused');
149+
140150
// What the boot actually holds.
141151
stack.metadata.object('crm_opportunity')?.fields; // system columns injected
142152
stack.metadata.items('permission'); // the registry's singular names
@@ -164,6 +174,22 @@ await stack.stop();
164174
touched, with `INVALID_REQUEST` / `400` (a caller named twice, or a
165175
`hooks.updateWhere` the engine would write by id). Assert on `code` (and
166176
`status` / `statusCode`), never on a message alone.
177+
- `observeWrites(fn)` records each insert, update and delete the engine
178+
receives while `fn` runs (`object`, `operation`, `data`, `where`, `multi`,
179+
`context`) and what the engine answered (`pending`, then `resolved` with its
180+
result or `refused` with its error and that error's own `code`). It is one
181+
middleware on the engine's own chain that passes everything on unchanged,
182+
so it sits after the write gates the boot registered: a write a gate refuses
183+
(the permission check among them) never reaches it and leaves no entry,
184+
while every refusal past the gates (a validation rule, a hook's throw, a
185+
unique conflict) is recorded. An outcome is the engine's answer to that call,
186+
not a statement about stored rows: read those back with `rows`. The window is
187+
time, not cause, so filter by `object`. `observation.settled(match)` waits
188+
for a write that lands later, such as an `async: true` hook's (default
189+
timeout 2000 ms). To stage a refusal of a hook's write, arrange one the
190+
engine makes itself: seed the row that already holds a unique value, or set
191+
up the state a declared validation rule refuses. The handle has no switch
192+
that refuses a write in the engine's place.
167193
- Many files, one boot: `bootStackOnce(config, opts?)` memoises `bootStack` per
168194
`(config, opts)` object identity for the life of the process. Share it from
169195
one module, under vitest `isolate: false`, and never `stop()` a stack other
@@ -221,7 +247,7 @@ run" must never read like "nothing to find".
221247

222248
- `bootStack(config, opts?)` → `VerifyStack` (`api` / `raw` / `signIn` / `signUp` / `apiAs` / `stop`, plus the handle:
223249
`hooks.run` / `hooks.updateWhere` / `validate` / `flows.run` / `flows.resume` / `actions.run` /
224-
`automation.evaluateCondition` / `seed` / `rows` / `metadata` / `tenancy` / `contextFor`).
250+
`automation.evaluateCondition` / `seed` / `rows` / `observeWrites` / `metadata` / `tenancy` / `contextFor`).
225251
- `bootStackOnce(config, opts?)` → the same, memoised per `(config, opts)` identity for the process.
226252
- `deriveCrudCases(config)` → the auto-derived round-trip cases (write one, read one, assert) for every object.
227253
- `runCrudVerification(stack, token, config)` → `VerifyReport`; `formatReport(report)` for a log summary.

0 commit comments

Comments
 (0)