Skip to content

Commit 7d648df

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22565-flow-cel-unbound-root
2 parents bf1c3a0 + 76bc1e0 commit 7d648df

65 files changed

Lines changed: 3455 additions & 225 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
A flow node's `outputVariable` (`get_record`, `create_record`, `map`, `script`, `subflow`) refuses a variable name that starts with `$`, and a `try_catch` node's `errorVariable` refuses every `$` name except the engine's own `$error`, its default. The refusal names the remedy: the same name without the `$`, read as `{{ name }}`.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered flow-binding-variable-dollar-name-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`).
12+
13+
**Why.** The `$` names are the flow engine's own variables: it binds `$record`, `$runId`, `$flowName`, `$flowLabel` and `$error`, a flat-graph `loop` binds `$loopItems` and `$loopIndex`, and a resume signal may not write any `$` name. A flow text slot already refuses a `{{ }}` hole whose root is a `$` name the engine does not bind, and tells the author to drop the `$`. But the binding keys took any string, so a flow could bind `errorVariable: '$caught'` and then be refused `'Failed: {{ $caught.message }}'`. The two doors of one contract gave two answers. Now both give the text slot's answer.
14+
15+
**What is refused.**
16+
17+
- `outputVariable` on `GetRecordConfigSchema`, `CreateRecordConfigSchema`, `MapConfigSchema`, `ScriptConfigSchema` and `SubflowConfigSchema`: a name that starts with `$`. That includes the engine's own names, since a binding over `$record` would overwrite the trigger record for the rest of the run.
18+
- `errorVariable` on `TryCatchConfigSchema`: a name that starts with `$`, other than `$error`.
19+
- Each key states the rule as a JSON Schema `pattern`, so the published `json-schema/**` refuses what the parse refuses. The parse issue is an `invalid_format` (regex) issue at the key, and its message names the remedy.
20+
- `FlowSchema.parse`, `registerFlow` and `objectstack validate` refuse the flow at `nodes.N.config.outputVariable` / `nodes.N.config.errorVariable`, inside a region body too (`nodes.N.config.try.nodes.M.config.outputVariable`). A stored flow carrying one is skipped at boot with a warn naming it, and each executor's own contract parse refuses the node at run time.
21+
22+
**Unchanged.** `errorVariable` absent (it defaults to `$error`) or an explicit `errorVariable: '$error'`; any name that does not start with `$`, a `$` later in the name (`a$b`) included; an empty string, which every executor reads as no binding; and every hole the text-slot judge already admits. Non-string values keep the type refusal they had.
23+
24+
## FROM → TO
25+
26+
| you wrote | write instead |
27+
|:--|:--|
28+
| `errorVariable: '$caught'` with `message: 'Failed: {{ $caught.message }}'` | `errorVariable: 'caught'` with `'Failed: {{ caught.message }}'`, or drop `errorVariable` and write `{{ $error.message }}` |
29+
| `outputVariable: '$lead'` with `title: '{{ $lead.name }}'` | `outputVariable: 'lead'` with `title: '{{ lead.name }}'` |
30+
31+
**The one-line fix: name the variable without the `$`, and rename every read of it with it.**
32+
33+
No D2 conversion rewrites the name: the bare name may already be bound in the flow, and the reads of the old name sit in every dialect a flow string speaks (a text-slot hole, a CEL expression, a single-brace token in a value position), so the rename is the author's.
34+
35+
**Who is affected, measured.** The last published spec, `@objectstack/spec@17.7.0` (npm `latest`), types all six keys as plain strings and accepts any `$` name. This repository was measured with `git grep` over `packages`, `examples`, `skills`, `apps`, `content` and `docs`: the only `$`-named `errorVariable` / `outputVariable` other than `$error` were three test fixtures, all renamed here (`errorVariable: '$err'` in two `packages/spec` automation tests, `errorVariable: '$caught'` in a `service-automation` test). Every authored `errorVariable` in examples and docs is `$error`, which stays legal, and no example or doc binds a `$`-named `outputVariable`. The pinned `objectui` checkout uses `errorVariable: '$error'` only. Deployed metadata and other repositories were not measured.
36+
37+
### The kit
38+
39+
- **The rule.** `flowBoundVariableNameSchema` in `automation/flow-bound-variable-name.ts`, package-internal (no new public export), composed into the six keys. It reads the `$` prefix the engine's closed list implies, not a copy of the list.
40+
- **The ledger.** The D3 semantic entry `flow-binding-variable-dollar-name-refused` (protocol 18), with no D2 conversion.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/cli': patch
3+
'@objectstack/driver-sql': patch
4+
---
5+
6+
fix(cli): `os migrate plan` / `apply` / `unmapped-columns` examine what `os serve` mounts around the stack, so the retired `sys_account.issuer` is finally a drop (#22506)
7+
8+
`os migrate plan` and `os migrate apply` only examine the objects their own boot registers, and that boot stopped at the stack: its config, its metadata and the platform floor. Nothing `os serve` mounts around a stack was there. On an app declaring `requires: ['auth']`, the plan examined 9 of the 68 tables the serving boot had created. `sys_account` read as an undeclared platform table, so its retired `issuer` column (and that column's unique index) was never a destructive drop. `os migrate apply --allow-destructive` dropped nothing, while the boot's drift line and `os migrate account-issuer` kept prescribing exactly that command.
9+
10+
The two commands now also compose, each through the rule `os serve` itself reads, and so does `os migrate unmapped-columns`, which reads the plan's own `unmapped_column` findings and therefore needs the plan's object set:
11+
12+
- the auth family behind `serve`'s auth gate: plugin-auth's identity objects, the security plugin and the audit plugin, when the stack mounts no `AuthPlugin` of its own, the `auth` tier is on, and an auth secret resolves;
13+
- the provider of every capability `serve` mounts: the stack's `requires` and the always-on slate, as `os serve` composes it with no `--preset`;
14+
- the REST API plugin;
15+
- every `plugins` entry by `serve`'s rule for one: a package name is loaded from the app, and a plain bundle is wrapped as an app;
16+
- the pinyin-search decision from the config's locales, so the `__search` companion columns a boot provisions are no longer listed as destructive drops on a config-only project.
17+
18+
`os migrate unmapped-columns --object sys_account` (or any platform object the plan now declares) resolves the object and reads its unmapped columns, where it used to answer `OBJECT_NOT_FOUND`.
19+
20+
Each piece is composed for its declarations only. Its `init()` runs, and its `start()` and lifecycle hooks do not, so no dispatcher, scheduler or seed runs inside a dry run.
21+
22+
**What an operator sees.** On the shape that looped, `os migrate plan` lists `sys_account.issuer` and `uniq_sys_account_issuer_account_id` as destructive drops. `os migrate apply --allow-destructive` drops both, `os migrate account-issuer` reads zero, and the next boot prints no drift line for them. When no auth secret is set and the boot is not a development one, the plan says it did not compose the auth family, and how to compose it: run with the deployment's `OS_AUTH_SECRET` exported. `os migrate` reads no `.env` file.
23+
24+
**driver-sql.** The deferred-DDL preview answers a rotation-declared object (`lifecycle.storage.strategy: 'rotation'`, such as `sys_activity`) from the rotator's facts: when the current shard and the read view exist, nothing is pending. It used to list `create_table` for such an object on every plan, and `apply` then created nothing.
25+
26+
**Known limit.** A development boot, or one with `OS_TELEMETRY_DB` set, keeps lifecycle-classed objects in a sibling `telemetry` database. The one-shot migration boot does not provision that database, so it examines those objects against the primary one.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`record:approvals` and `record:attachments` are declared page component types — the record page's Approvals and Attachments panels — each with a `ComponentPropsMap` row that accepts no props
6+
7+
Clause-②: yes (widening)
8+
9+
- **Two new `PageComponentType` members:** `record:approvals` (the record's approval timeline: the step its approval sits at, who it waits on, what has been decided) and `record:attachments` (the record's attached files, with upload, download and delete). objectui already renders both, and its default record page places them: the Attachments tab on every record page of an `enable.files: true` object, the Approvals tab on a record that has approval requests.
10+
- **The page Studio's page create seeds now passes `os validate`.** Before, `record` was a reserved namespace with neither type declared, so `os validate` / `os build` / `os lint` refused the seeded page of any `enable.files` object with `component-type-unknown` on its `record:attachments` node. The metadata save door did not refuse it and still does not.
11+
- **Both rows are strict and accept no key.** A node with no `properties`, or `properties: {}`, passes. Any key inside `properties` is reported as `component-props-unknown-key`, naming the component and the key. Node-level keys (`id`, `className`, `visibleWhen`, …) stay on the node as for every component.
12+
- **`record:approvals` refuses the runtime channel it reads, with the fix.** Its renderer reads `approvals` (the approval requests the default record page fetched) and `currentUserId` (the signed-in user) from the node, and both are filled by the host at runtime. An authored `approvals` would show a static approval history that never updates, and an authored `currentUserId` would decide for every viewer who counts as the submitter. Both are refused with a message that says to omit them: the block fetches the record's own approval requests and reads the signed-in user itself.
13+
- **A misspelling is refused.** `record:attachment`, `record:aprovals` and other near spellings are `component-type-unknown` errors that offer the declared spelling.
14+
- **Not printable.** Inside a page that declares `print`, each type is refused with its own reason, in place of the generic "not in the printable block subset" one.
15+
- **Consumers.** A `kind: 'react'` page that names `<RecordApprovals>` or `<RecordAttachments>` is now refused by `react-block-needs-record-context`, as every `record:*` block is: these blocks read the record context a record page mounts, and a react page mounts none. A consumer that derives coverage from `PageComponentType.options` or the keys of `ComponentPropsMap` — a designer palette, a renderer registry — sees two more members to classify.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix(plugin-approvals): the generic data door serves `sys_approval_request` rows by the approvals door's visibility
6+
7+
Clause-②: no
8+
9+
A read of `sys_approval_request` through the generic data door (`find`, `findOne`, `count` and `aggregate`, so the REST data routes, the export route, the MCP data tool and every other engine read) now returns only the requests the approvals door (`/api/v1/approvals/*`) serves the same caller:
10+
11+
- the requests the caller submitted, is a current approver of, or has acted on;
12+
- every request, for an administrator holding the approval override;
13+
- read-only, on an object named in `recordReaderVisibleObjects`, the requests of a record the caller can read, on a read that names that record (`object_name` and `record_id`, or the request's id).
14+
15+
Before this, a deployment that granted read on the object served every request of the organization on that door, including the payload snapshots of records the caller cannot open.
16+
17+
- **One rule for both doors.** The gate asks the approvals service for its own visibility (the same rule `listRequests` and `getRequest` apply), through an in-package handle: the published `ApprovalService` type is unchanged. It is not the activity stream's parent-record gate, which admits every reader of the record: the record-reader tier stays off unless the deployment names the object.
18+
- **Absence, not refusal.** A request the caller may not see answers a by-id read exactly as a missing one does, `404 RECORD_NOT_FOUND`. A list's `total` and a grouped count are narrowed like its rows. A failed visibility answer denies the read and logs a warning.
19+
- **Unchanged.** The approvals door, an administrator's view (Setup → Approvals → Requests), and the approval engine's own reads. A deployment that grants no read on the object still answers `403`.
20+
- The record-reader tier no longer anchors on `sys_approval_request` itself, even when `recordReaderVisibleObjects` names it.

0 commit comments

Comments
 (0)