You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`os generate migration` now gives a `multiple: true` field a JSON column, in both formats. One authored field used to produce two incompatible answers from one config in one run: `Field.lookup({ reference: 'account', multiple: true })` emitted `account?: string[]` from `os generate types` and a scalar `VARCHAR(36)` / `table.uuid('account')` column from the two migration generators, because `multiple` appeared exactly four times in `generate.ts` and all four were on the TypeScript side — `fieldTypeToSql` did not even take the parameter. Nothing warned: the scaffold looks right, the generated TypeScript IS right, and only the column is wrong, so the first symptom was a write of an array into a scalar column. That is the `#field-zoo` failure one layer out — there the DDL switch and `isJsonField` had drifted into two lists inside the driver; here the platform and the *generated* DDL were the two lists.
6
+
7
+
The authority is the driver's, and it is the flag alone. `SqlDriver.createColumn` short-circuits on `field.multiple`**above** its own `switch (type)`; `isJsonField` is `JSON_COLUMN_TYPES.has(type) || !!field.multiple`; and `fieldHasColumn` opens with `if (field?.multiple) return true` under the comment "Mirrors `SqlDriver.createColumn` exactly … including `multiple` (a JSON column)". Three statements of one rule: a flagged field is a JSON column whatever its element type would have been. Both generators now answer it the same way and in the same place — before the type is consulted at all.
8
+
9
+
Deliberately **not** the spec's `isMultiValueField`. That predicate is the ADR-0104 D1 *value* contract ("is the persisted value an array") and gates on `MULTI_CAPABLE_TYPES`, so asking it here would answer `VARCHAR` for a `text` field flagged `multiple: true` while the driver gives that same field a JSON column — the identical drift one notch narrower. `FieldSchema` does not refuse the combination either (`multiple` is a plain `z.boolean()` on every field; only `radio` + `multiple` is refused by name), and the generators sit downstream of validation. The two questions have two different owners: the value shape is the spec's, the column is the driver's.
10
+
11
+
Nothing about the existing per-type vocabularies changes. The scalar answers — including the five that are separately disputed — are byte-for-byte what they were, and a new pin asserts that as a scope fence rather than leaving it to a reading of the diff. `generate-multiple-json-column.pin.test.ts` drives all three generators on one config and pins the agreement across every member of the spec's `MULTI_CAPABLE_TYPES` plus a type outside it, so the type-blindness of the rule is an assertion rather than a comment; it also reads the driver's two statements of the rule, so moving them there fails here.
feat(hooks): `runAs` on a hook — `'system' | 'user' | 'inherit'`, default `'inherit'`
8
+
9
+
A hook's `ctx.api` runs with the context of the write that fired it, so a column
10
+
an app wants **computed and never hand-written** could not be expressed: author
11
+
`editable: false` for the persona and the direct `PATCH` is refused — and so is
12
+
the hook that maintains the column, by the same field-level check. The guard and
13
+
the legitimate writer were the same door. The only elevation a hook had was the
14
+
in-process `ctx.api.sudo()`, which is not marshalled into the sandbox (a
15
+
`TypeError` once a build lowers the handler into a body) and which rides the L3
16
+
bundle path that is being retired.
17
+
18
+
`HookSchema` now accepts `runAs`:
19
+
20
+
| value | the hook's `ctx.api` data operations run as |
21
+
| --- | --- |
22
+
|`'inherit'` (default) | the context of the triggering write — exactly the behaviour every hook has today |
23
+
|`'system'`| elevated: a full-access, RLS-bypassing system principal |
24
+
|`'user'`| the triggering user; a hook whose trigger resolved no user has its data operations **refused** (`HOOK_UNSCOPED_DATA_ACCESS`) rather than run unscoped |
25
+
26
+
`'system'` and `'user'` mean here exactly what they mean on `flow.runAs` — same
27
+
word, same semantics. `'inherit'` is the hook-only third value, because only a
28
+
hook has a context to inherit; a flow establishes its identity from nothing,
29
+
which is why its default is `'user'` and this one's is `'inherit'`. Nothing on
30
+
`FlowSchema` changes.
31
+
32
+
**Purely additive: no migration, no behaviour change for any existing hook.**
33
+
The default reproduces today's behaviour by handing the engine-built `ctx.api`
34
+
through unchanged, and an absent key parses to it.
35
+
36
+
Scope, deliberately narrow: `ctx.api` data operations only. `condition`
37
+
evaluation, the `readonly` strip applied to the hook's own `ctx.input` payload,
38
+
`ctx.session` and `async` semantics all keep reading the triggering operation's
39
+
context, and declaring `runAs: 'system'` does not elevate the write that fired
40
+
the hook.
41
+
42
+
Elevation is authorization, not anonymity: a `runAs: 'system'` write still
43
+
carries the triggering user, so `created_by` / `updated_by` and the audit row
44
+
still name the operator.
45
+
46
+
Honoured on both execution surfaces — the in-process `handler` and the
47
+
sandboxed `body`.
48
+
49
+
Authoring notes:
50
+
51
+
-`sudo`, `elevate`, `elevated` and `isSystem` are refused with a prescription
52
+
naming `runAs`, and `run_as` is answered as a rename.
53
+
-`@objectstack/lint`'s gating `hook-api-update-readonly-field` rule now skips a
54
+
hook that declares `runAs: 'system'` — the static `readonly` strip skips a
55
+
system context, so the write it exists to catch does not happen — and its
56
+
hints name the knob. The `readonlyWhen` warning is unchanged: a system context
0 commit comments