Skip to content

Commit c2fad52

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/3426-followups
# Conflicts: # packages/cli/src/commands/validate.ts # packages/lint/src/index.ts
2 parents e9924d8 + 7f4a8a1 commit c2fad52

71 files changed

Lines changed: 3242 additions & 128 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/service-storage": patch
4+
"@objectstack/client": patch
5+
---
6+
7+
feat(storage): surface the sys_file id on upload-complete — ADR-0104 D3 wave 2 (PR-1)
8+
9+
`POST /api/v1/storage/upload/complete` now returns the opaque `sys_file` id
10+
(`data.fileId`), and `client.storage.upload()` surfaces it on the returned
11+
`FileMetadata`. Previously the commit response omitted the id — the caller
12+
could not learn which id to persist after committing an upload, so a file
13+
field could never store a reference.
14+
15+
Additive and non-breaking (new optional `fileId` on `FileMetadataSchema`; the
16+
client falls back to the presigned id when talking to an older server). This is
17+
the enabling foundation for file-as-reference; the storage model itself is
18+
unchanged in this PR.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
feat(objectql): resolve file-field id references on read — ADR-0104 D3 wave 2 (PR-2)
6+
7+
The engine read path now resolves a `file`/`image`/`avatar`/`video`/`audio`
8+
value stored as an opaque `sys_file` id string into its expanded
9+
`FileValueSchema` form — `{ id, name, size, mimeType, url }`, with `url` derived
10+
from the stable `/api/v1/storage/files/:fileId` resolver (never stored). One
11+
batched `sys_file` `id $in […]` read per query (no N+1), mirroring the
12+
lookup-`$expand` batch pattern.
13+
14+
**Dual-mode safe.** An inline-blob value (an object) passes through unchanged,
15+
and only an **opaque id token** (uuid/nanoid-shaped) is treated as a reference —
16+
a URL-shaped value (`https://…`, `/api/…`, `data:…`, `blob:…`), which a file
17+
field legitimately holds in the legacy world, is never looked up. The step
18+
fires zero reads unless a file field actually holds an id token (the blob/URL
19+
case is free), and it no-ops entirely when `sys_file` is not registered.
20+
21+
This makes a stored `fileId` (surfaced by PR-1) actually usable on read, ahead
22+
of the v17 cutover that narrows the stored form to an id.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/plugin-approvals": patch
3+
"@objectstack/spec": patch
4+
---
5+
6+
fix(approvals): admin override for a request routed to an unstaffed approver (#3424)
7+
8+
An `approval` node routed to a `position` (or `team`/`department`) with **no
9+
holders** resolved to only the unresolvable `position:<name>` literal in
10+
`pending_approvers` — no concrete user was in the slate. Every normal
11+
`decide` / `reassign` / `recall` then returned `FORBIDDEN` (not a pending
12+
approver) and, with `lockRecord`, the target record stayed `RECORD_LOCKED`
13+
forever: a data-availability dead-end with no in-product recovery (the only exit
14+
was editing the DB by hand). Very easy to hit in fresh/demo orgs (positions
15+
seeded, holders not) and whenever a role is vacated in production.
16+
17+
A **platform or tenant admin** — the same posture the engine's superuser bypass
18+
already trusts — may now act on any *pending* request to release it: **approve,
19+
reject, reassign** it to a real approver, or **recall** it. The override finalizes
20+
the request (which releases the record lock, keyed on a pending request); a
21+
tenant admin's authority is org-scoped, a platform admin's is not, and the
22+
decision is audited under the admin's own id. An admin approval is authoritative,
23+
finalizing the node even under `unanimous` / `quorum` / `per_group` rather than
24+
counting as one vote among the (empty) slate.
25+
26+
- `sys_approval_request.viewer` gains `can_override` (server-computed): true for a
27+
privileged admin on a pending request. The `approve` / `reject` / `reassign`
28+
declared actions OR it into their `visible` gate, so the console surfaces the
29+
recovery path without a hand-wired button. Existing approver/submitter gating is
30+
unchanged.
31+
- `openNodeRequest` now logs a loud warning when a node resolves to **no concrete
32+
approver**, so the misconfiguration is visible instead of silently locking the
33+
record. The literal-fallback behavior (kept for 15.x slot back-compat) is
34+
otherwise unchanged.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/plugin-approvals": patch
4+
---
5+
6+
feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807)
7+
8+
`per_group` (会签) requests now carry `pending_approver_groups` on the
9+
enriched row — a map from each still-pending approver id to the group key(s)
10+
it fills (e.g. `{ "u_devadmin": ["finance", "legal"] }`). A client can label
11+
each "waiting on" chip with the group it represents instead of showing
12+
duplicate, context-free names.
13+
14+
- Resolved in `attachDecisionProgress` from the same open-time
15+
`__approverGroups` snapshot the `decision_progress` groups already use, so
16+
the two never disagree.
17+
- Only the **pending** slots are mapped (a resolved approver has left
18+
`pending_approvers`), and **synthetic** (unnamed, `#N`) group keys are
19+
dropped — a `· #0` sub-tag would be noise.
20+
- Absent for non-`per_group` behaviors. Display-only; the engine's
21+
finalization tally stays authoritative.
22+
- Added to the `ApprovalRequestRow` contract in `@objectstack/spec`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): flag never-firing record trigger tokens at authoring time (#3427)
6+
7+
New `flow-trigger-unknown-event` rule in `validateFlowTriggerReadiness`: a flow
8+
start node whose `triggerType` is record-lifecycle-shaped
9+
(`record-before|after-<op>`) but names an op the record-change trigger cannot map
10+
— e.g. a typo like `record-after-updated` — binds to the record-change trigger
11+
yet maps to no ObjectQL hook and never fires, with only a runtime warning. The
12+
rule surfaces that never-fire defect at `os validate` time. Warning severity;
13+
bare `record-<noun>` shapes (e.g. `record-change`) are out of scope.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): flag every never-firing `record-`-prefixed trigger token, incl. `record-change` (#3427)
6+
7+
Generalizes the `flow-trigger-unknown-event` rule: it now flags ANY `record-`-prefixed
8+
`triggerType` that is not a valid firing token
9+
(`record-{before,after}-{create,insert,update,delete,write}`) — not just
10+
`record-(before|after)-<bad-op>` typos. This closes the `record-change` trap: the
11+
engine routes `record-change` ("Record changed (any)") to the record-change trigger,
12+
which maps it to no hook so it never fires — now caught at `os validate` time instead
13+
of only a runtime warn. Also covers bad-phase tokens like `record-during-update`.
14+
Warning severity, unchanged.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): bind `previous` (as null) on the create leg so start conditions can discriminate create vs update (#3427)
6+
7+
The engine bound `previous` into the flow condition scope only when it was
8+
truthy, so on a record insert (`record-after-create`, and the create leg of
9+
`record-after-write`) `previous` was an **unknown** CEL variable. Any reference to
10+
it — including the documented `previous == null` create-discrimination — threw
11+
`condition failed to evaluate as CEL: Unknown variable: previous`, failing the
12+
whole start condition and dropping the run.
13+
14+
`previous` is now always bound, to `null` when there is no prior row. So
15+
`previous == null` is the create leg and `previous != null` / `previous.<field>`
16+
the update leg — the pattern the `record-after-write` docs and the Studio flow
17+
designer advertise. Update-triggered flows are unaffected (`previous` was, and
18+
stays, the prior row there).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/lint": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
feat(lint,cli): flag flow `update_record` writes to readonly fields at design time (#3425)
7+
8+
A flow `update_record` node that writes a field the target object declares
9+
`readonly: true`, under the default `runAs: 'user'` identity, is a **silent
10+
no-op**: the objectql engine strips static-`readonly` fields from a non-system
11+
UPDATE payload (#2948), so the intended write never lands — yet the step still
12+
reports `success`. #3407/#3413 surfaced the strip as a run-time step warning;
13+
this moves the discovery **left** to `os validate` / `os build` so an author
14+
finds the mismatch at design time instead of by reading server WARN logs days
15+
later.
16+
17+
- New `@objectstack/lint` rule `validateReadonlyFlowWrites(stack)` — a pure
18+
`(stack) => Finding[]` check (ADR-0019). A static `readonly:true` field
19+
written by a literal `update_record` under `runAs !== 'system'` is a
20+
100%-certain no-op → **error** (gates the build). A `readonlyWhen` field is
21+
per-record-state → **warning** (advisory). Deliberately narrow to stay
22+
false-positive-free: `create_record` (INSERT is engine-exempt from the strip),
23+
`runAs: 'system'` flows (the intended "automation maintains it" channel),
24+
templated object names, and non-literal `fields` maps are all skipped.
25+
- Wired into `os validate` and `os compile`/`os build`, mirroring the existing
26+
security-posture gate (errors fail; advisories print dimmed).
27+
28+
The formal contract, unchanged in behavior: `readonly` governs the end-user /
29+
API surface (REST/UI and `runAs:'user'` flows strip it); trusted system writers
30+
(`runAs:'system'`, system hooks, seeds) maintain it. To let a flow maintain a
31+
readonly field, declare `runAs: 'system'`.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/lint": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
feat(lint): warn on replay-unsafe `mode: 'insert'` seed datasets (#3434 follow-up)
7+
8+
Seeds are replayed — they re-load on every dev-server boot and every package
9+
re-publish, not applied once — so `mode: 'insert'` (the loader's one mode with
10+
no existing-row check) duplicates its table on every restart. That footgun
11+
shipped undetected until #3434 (showcase memberships grew 3 → 6 → 9).
12+
13+
Adds `validateSeedReplaySafety` to `@objectstack/lint` (a pure `(stack) => Finding[]`
14+
rule, ADR-0019) and wires it into `os validate` / `os lint`. Every `data[]` seed
15+
declared with `mode: 'insert'` now gets an advisory warning that points at the
16+
idempotent modes (`ignore` / `upsert`) and the `externalId` to match on — a
17+
single natural-key field, or a COMPOSITE list of fields for a join / junction
18+
table with no single key (`['team', 'project']`, the support #3434 added). It
19+
catches the mistake at authoring time instead of on the second boot.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/lint": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
feat(lint): warn on seed values outside an object's declared state machine (#3433 follow-up)
7+
8+
#3433 exempts seed writes from the `state_machine` validation rule, so a seeded
9+
status the FSM does not declare is no longer rejected at write time. A field-level
10+
`select` still catches a value outside its `options`, but a `state_machine` on a
11+
free-text field — or a value that is a valid option yet not a declared FSM state —
12+
now sails through silently: the exemption is a deliberate but blind back door.
13+
14+
`validateSeedStateMachine` (a pure `(stack) => Finding[]` rule, run from
15+
`os validate` / `os lint`, symmetric with the replay-safety rule from #3434)
16+
re-adds that safety net at author time. It flags any seed record whose
17+
`state_machine`-governed field carries a value outside the machine's declared
18+
states — the union of `initialStates`, the transition-map keys, and the transition
19+
targets. Advisory (`warning`): the exemption itself is legitimate, so the fix-it
20+
points at either adding the state to the machine or correcting the typo, not a hard
21+
build failure. New rule id: `seed-value-outside-state-machine`.

0 commit comments

Comments
 (0)