Skip to content

Commit 1c502d3

Browse files
Merge branch 'main' into claude/issue-15204-s6b1a-capability-declarations
2 parents cca8fe4 + 243dd3c commit 1c502d3

191 files changed

Lines changed: 13499 additions & 9688 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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@objectstack/spec': major
3+
'@objectstack/lint': major
4+
---
5+
6+
feat(spec)!: an element binds data through the node-level `dataSource` only — the flat `object` / `filter` / `sort` / `limit` of `element:record_picker`, `element:number` and `element:repeater`, and `object-grid.defaultFilters`, are retired (#11509)
7+
8+
Clause-②: no (narrowing: the ten element-layer flat data-binding keys and `object-grid.defaultFilters` leave the accept set, and the component-props gate's `dataSource.object` waiver becomes a refusal)
9+
10+
<!-- adr-0087: registered element-flat-data-binding-retired, object-grid-default-filters-retired -->
11+
12+
**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.*`). `@objectstack/lint` is `major` with it: its component-props rule refuses what it used to waive.
13+
14+
**Why.** One page element carried two doors onto one query. Each of these flat keys was the same query as a key of the node-level `dataSource` binding (`ElementDataSourceSchema`), and the three renderers resolved them by three contradictory rules: the record picker let the binding win, `element:number` AND-combined the two filters, and the repeater read the flat keys alone and ignored the binding — while the component-props rule waived a missing flat `object` whenever `dataSource.object` was present, so a repeater bound only through `dataSource` passed `os validate` and drew "No records". The console moved all three elements onto the binding first (objectui#11880); from this release the binding is the one door. `object-grid.defaultFilters` was the legacy second spelling of `filter`, read only when `filter` lowered to nothing.
15+
16+
**What changes.**
17+
18+
- **`@objectstack/spec`.** `ElementRecordPickerPropsSchema` `object` / `filter` / `sort` / `limit`, `ElementNumberPropsSchema` `object` / `filter`, `ElementRepeaterPropsSchema` `object` / `filter` / `sort` / `limit` and `ObjectGridPropsSchema` `defaultFilters` are `retiredKey()` tombstones: each is refused at its key with the prescription, and its input type is `never`. The repeater's other spellings of its query keys (`objectName`, `where`, `orderBy`, `top`, …) now point at the binding. The `object-*` blocks keep their own keys, and the relationship-scoped blocks are unchanged.
19+
- **`@objectstack/lint`.** `validate-component-props` (`component-props-invalid`, warning) reports an `element:record_picker`, `element:number` or `element:repeater` node with no `dataSource.object` at that path, instead of waiving the flat `object` for any type whose binding names one. A flat key is reported by its tombstone.
20+
- **Conversions (stored rows, artifacts, `os migrate meta --from 17`).** `element-flat-data-binding-to-data-source` moves a flat key the binding lacks onto it, deletes one the binding already set where the binding won, and appends `element:number`'s flat filter to the binding's (they always AND-combined). It leaves for the author, as a TODO: a record-picker key beside a `dataSource.view` that sets no such key of its own, a repeater key the binding sets to a different value or beside a `view`, and an `element:number` filter pair that is not two rule arrays. `object-grid-default-filters-removed` moves `defaultFilters` onto an empty `filter` (absent, `null`, `[]` or `{}`) and deletes it beside a `filter` that has rules. Both run before `page-component-filter-record-to-rule-array`, which then converts a moved record-form filter at its new door. Both are retired from the load path: an author is refused at the parse.
21+
22+
## FROM → TO
23+
24+
| you wrote | write instead |
25+
|:--|:--|
26+
| `{ type: 'element:record_picker', properties: { object: 'deal', limit: 20, labelField: 'name' } }` | `{ type: 'element:record_picker', dataSource: { object: 'deal', limit: 20 }, properties: { labelField: 'name' } }` |
27+
| `{ type: 'element:number', properties: { object: 'deal', aggregate: 'count', filter: [...] } }` | `{ type: 'element:number', dataSource: { object: 'deal', filter: [...] }, properties: { aggregate: 'count' } }` |
28+
| `{ type: 'element:repeater', properties: { object: 'deal_note', sort: [...], titleField: 'subject' } }` | `{ type: 'element:repeater', dataSource: { object: 'deal_note', sort: [...] }, properties: { titleField: 'subject' } }` |
29+
| `object-grid` `properties: { defaultFilters: [...] }` (no `filter`) | `properties: { filter: [...] }` |
30+
| `object-grid` `properties: { filter: [...], defaultFilters: [...] }` | `properties: { filter: [...] }` — the fallback was never read beside a `filter` with rules |
31+
32+
**The one-line fix: move each key from `properties` to the node's `dataSource` (a sibling of `type`), unchanged; rename `defaultFilters` to `filter` where `filter` is empty, and delete it where it is not.**
33+
34+
**For a consumer that pins both repositories:** its objectui pin moves past objectui#11880 no later than its objectstack pin moves past this release — the converted shape is one only that objectui reads.
35+
36+
**Who is affected, measured.** This repository authors none of the eleven keys: its one element-layer author (the showcase record picker) already binds through `dataSource`, and a tree-scoped absence pin (`packages/spec/src/ui/element-flat-binding-retirement.test.ts`) keeps it that way. Other repositories and deployed metadata were not measured here.
37+
38+
### The retirement kit
39+
40+
- `RETIRED_KEYS_BY_MAJOR[18]`: `ui/ElementRecordPickerProps:object|filter|sort|limit`, `ui/ElementNumberProps:object|filter`, `ui/ElementRepeaterProps:object|filter|sort|limit`, `ui/ObjectGridProps:defaultFilters`. D3 entries `element-flat-data-binding-retired` and `object-grid-default-filters-retired`, each with a step-18 rationale fragment. They absorb the three protocol-18 narrowings of the same keys to the rule array (`element-number-filter-rule-array`, `element-record-picker-filter-rule-array`, `object-grid-default-filters-rule-array`), whose keys are gone in the same major; `page-component-filter-record-to-rule-array` drops the retired doors from its reach.
41+
- Generated: `authorable-surface/ui.json` (11 rows `[RETIRED]`) and the `ui/component` reference page. Hand-edited ledger: `dropped-refinements.baseline.json` loses the three rows whose only dropped refinement was a retired `filter`.
42+
- Pins: the tombstones, the binding, both conversions and the tree-scoped absence walk in `element-flat-binding-retirement.test.ts`; the missing-binding refusal and the repeater trap in `validate-component-props.test.ts`; the docs gate's twin in `check-yaml-examples.ts --self-test`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the RLS-predicate and sharing-rule findings print one verdict line, and `os explain <rule-id>` carries their reasoning
6+
7+
Clause-②: no
8+
9+
- **Shorter verdicts.** Each finding of these 9 rule ids now prints a `message` of one verdict sentence. Every finding the rules' own test suites, the CLI's RLS admission suite and the runtime publish gate's suite fire is at most 195 characters; before, the longest of each ran from 492 to 2,056 characters. The ids:
10+
- RLS policies (`permissions[].rowLevelSecurity[].using` / `.check`): `rls-predicate-unparseable`, `rls-predicate-unenforceable`, `rls-predicate-unknown-user-variable`, `rls-predicate-unknown-field`, `rls-predicate-over-budget`;
11+
- sharing rules (`sharingRules[]`): `sharing-rule-unlowerable-condition`, `sharing-rule-object-not-shareable`, `sharing-rule-object-controlled-by-parent`, `sharing-rule-runtime-variable-condition`.
12+
13+
A finding no longer echoes the predicate or the condition: its `path` names the clause, and the predicate is author text of any length (the old message quoted up to 200 characters of it, or all of it). Each verdict names what is wrong and closes on what it costs: an RLS policy the runtime drops ends "so the policy is dropped and grants no access" (a `using`) or "… and admits no write" (a `check`); a field compared with a field that holds a list or an object, or across comparison classes, quotes the first such comparison, names the column or the two classes, counts the rest, and ends on the `INVALID_FILTER / 400` refusal; a sharing condition that does not lower ends "so the rule is never seeded and grants nothing"; an anchor object a grant cannot widen names its `sharingModel` and the `SHARING_NOT_ENABLED` refusal. A quoted refusal — the compiler's, the platform's shared filter check's, or the engine's (`RLS using (CODE / STATUS): …`) — is quoted up to its own suggested rewrite, or to its first sentence. The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
14+
- **`os explain <rule-id>` takes these 9 ids**, for example `os explain rls-predicate-unenforceable`. It prints the reasoning the verdicts no longer carry: what `RLSCompiler` does with a policy it cannot compile (the deny sentinel and its ZERO rows, the OR with other policies, the INSERT check a `using` doubles as, the `check` set), the one WARN line each drop leaves, which `current_user` keys the kernel resolves and with which type, why a column that holds a list or an object — or two columns of different comparison classes — cannot be compared, what the engine's own filter admission refuses and what that costs analytics, and, for sharing rules, why a condition is compiled once at boot, what an unlowerable or refused condition leaves behind, and why a grant on a public or `controlled_by_parent` object is refused. The paragraphs the RLS and sharing rules share are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 9 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 9 entries.
15+
- **Where the new text prints.** On the CLI, all 9 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify` and `os init`'s scaffold check print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP), a `permission` write carries the five `rls-predicate-*` ids, all errors: the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change. Each issue's `hint` is unchanged.
16+
- **Never at the runtime gate:** the four `sharing-rule-*` ids. The rule is registered for the CLI doors only (no write type dispatches it at the publish gate), so they speak only on the CLI doors above.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-storage': patch
4+
'@objectstack/client': minor
5+
---
6+
7+
feat(spec,service-storage,client)!: one upload-scope list — the upload requests' `scope` and the SDK's `storage.upload` close to the new `UploadScope` enum, the `sys_file` scope select is built from it, and the upload doors answer any other scope with `400` naming the allowed values instead of `500 INTERNAL` (#22470)
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: registered upload-request-scope-closed -->
12+
13+
**BREAKING** — an accept-set narrowing on a published request contract and on the SDK method that sends it, shipped as `minor` under the launch-window convention for accept-set narrowings, plus one new export.
14+
15+
The presigned and chunked upload requests declared `scope` an open string, while the stored file record (`sys_file.scope`, a closed select) only ever took `user`, `tenant`, `private`, `temp` and `attachments`. Any other value reached the `sys_file` insert, the data engine refused it as an invalid option, and the upload door answered that caller error as `500 INTERNAL`, with a message telling the operator to restore the data engine.
16+
17+
### What changes
18+
19+
- **`UploadScopeSchema` / `UploadScope`** (`@objectstack/spec`, new, exported from `@objectstack/spec/api`): the upload-scope vocabulary, declared once — `user`, `tenant`, `private`, `temp`, `attachments`. It is a different list from `StorageScopeSchema`, which classifies a storage configuration and is not read by any upload.
20+
- **`GetPresignedUrlRequestSchema.scope` and `InitiateChunkedUploadRequestSchema.scope`** read it. The default stays `user`. A literal outside the list fails `tsc`, and a parse refuses it on the `scope` key.
21+
- **`client.storage.upload(file, scope)`** (`@objectstack/client`): the `scope` parameter is typed `UploadScope` instead of `string`, default `user` unchanged, so a scope outside the list fails `tsc` at the SDK call. `client.storage.getPresignedUrl` and `client.storage.initChunkedUpload` take the request types above and narrow with them.
22+
- **The `sys_file` scope select** (`@objectstack/service-storage`) takes its options from the enum, in its order, with the same labels. The stored values do not change.
23+
- **The presigned upload door and the chunked upload door** answer a scope outside the list — any string, a case variant, `null`, a number — with `400 INVALID_REQUEST`, naming the allowed values, before a file record, a session record, an upload URL or a backend upload exists. `public` is refused by the same gate and keeps its remedy (`acl: 'public_read'` on the stored file record). An omitted scope is the default `user`, as before. A real data-engine fault still answers `500`.
24+
25+
### FROM → TO
26+
27+
| before | what to write instead |
28+
| --- | --- |
29+
| an upload naming a scope outside the list (a key prefix such as `avatars`, a folder, a record path) | one of `user`, `tenant`, `private`, `temp`, `attachments`, or no `scope` for the default `user` |
30+
| `client.storage.upload(file, scope)` called with a `scope` typed `string` (`@objectstack/client`) | pass one of the five, or type the value `UploadScope` (`import type { UploadScope } from '@objectstack/spec/api'`) |
31+
| a caller passing a scope typed `string` into either upload request | type it `UploadScope` |
32+
33+
**The one-line fix: send one of the five upload scopes, or none.** `attachments` is for a file whose referrers are record attachment rows (it is what orphan tombstoning reads); `temp` for a scratch file; otherwise `user` or `tenant`.
34+
35+
**No stored file record moves**: no upload naming another scope ever succeeded, so no stored record carries one.
36+
37+
### The kit
38+
39+
- **D3 entry `upload-request-scope-closed`** carries the judgement no rewrite can make: which scope a file that was sent under a free name really belongs under. No D2 conversion: no metadata type carries the upload request, so `os migrate meta` has no authored source to rewrite.
40+
- **Liveness.** No ledger row: the liveness ledger walks metadata types, and no metadata type carries the upload request.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
---
4+
5+
`sys_activity.actor_name` is written: a user's create, update or delete records that user's display name on its activity row, so the record History tab names who acted instead of "Unknown user"
6+
7+
Clause-②: no
8+
9+
`sys_activity` declares `actor_name` and lists it among its highlight fields, because activity
10+
entries are denormalized snapshots read in time order. The audit writer filled `actor_id` and never
11+
`actor_name`, so every row carried the user's id and no name. A renderer that reads the name, such
12+
as the record History tab, showed "Unknown user" for every entry, for every user.
13+
14+
The writer now records the name when it writes the row. The name is the title of the user's
15+
`sys_user` row (its `name` column), read once per user and kept for 30 seconds. A predicate write
16+
over many rows costs one `sys_user` read for its actor, not one per row.
17+
18+
- **A user write.** `actor_name` is the user's display name. `actor_id` is unchanged, and the name
19+
is read for that same id. This includes a write the system authorized on behalf of a credited
20+
user (`attributedUserId`).
21+
- **A user whose name cannot be read** (no row, or a blank name). `actor_name` stays empty. The
22+
writer never puts the id in it.
23+
- **A write with no user** (boot, migration, a scheduled job, a flow running as the system).
24+
`actor_name` stays empty, as before. Per ADR-0118 D1 the system actor is `null`, and the UI
25+
renders the "System" label for it.
26+
- **An object with `enable.activities: false`.** No activity row is written and no name is read,
27+
as before.
28+
29+
Rows written before this change keep `actor_id` and no name. This change does not backfill them.
30+
Nothing you author changes: no key, option, export or type is added or removed.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
---
4+
5+
`sys_activity.actor_avatar_url` is written: a user's create, update or delete records that user's profile image on its activity row, so the record History tab and the activity feed show the user's avatar instead of the generic one
6+
7+
Clause-②: no
8+
9+
`sys_activity` declares `actor_avatar_url` as one of its denormalized actor columns, and the record
10+
History tab and the record activity feed both read it as an image source. The audit writer never
11+
filled it, so every row carried a null avatar, even for a user with a profile image.
12+
13+
The writer now copies the acting user's `sys_user.image` (the profile-image URL that
14+
`POST /api/v1/auth/update-user` and the console's avatar uploader write) into `actor_avatar_url`
15+
when it writes the row. The value is copied as stored. It comes from the same `sys_user` read that
16+
already supplies `actor_name`, read once per user and kept for 30 seconds, so the avatar adds no
17+
read: a predicate write over many rows still costs one `sys_user` read for its actor.
18+
19+
- **A user with a profile image.** `actor_avatar_url` is that image's URL.
20+
- **A user with no profile image.** `actor_avatar_url` stays empty, as before.
21+
- **A write with no user** (boot, migration, a scheduled job, a flow running as the system).
22+
`actor_avatar_url` stays empty, as before. Per ADR-0118 D1 the system actor is `null`, and how
23+
the system is shown is the renderer's choice.
24+
25+
Rows written before this change keep their empty avatar; a later change to a user's image shows on
26+
rows written after it, as with the name.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
Studio's object form now offers the `approvalsVisibleToReaders` row in its capabilities panel, with its label and help text in the metadata-form translation catalogs for `en`, `zh-CN`, `ja-JP` and `es-ES`.
6+
7+
Clause-②: no
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-approvals': minor
3+
---
4+
5+
feat(plugin-approvals): the record-reader approval tier honours an object's own `enable.approvalsVisibleToReaders` declaration
6+
7+
Clause-②: yes (widening)
8+
9+
- **Before this,** the read-only record-reader tier could be switched on only through the `recordReaderVisibleObjects` constructor option. A config-driven app, whose host builds the plugin with no options, had no way to reach it.
10+
- **Now** the tier is on for an object when either source turns it on: the host's `recordReaderVisibleObjects`, or the object's own `enable.approvalsVisibleToReaders: true`. With neither, visibility is exactly as before.
11+
- **Read where it is used.** The service reads the declaration from the object's live registered definition on each read that names a record, not once at start. An object registered after boot (an installed package, a Studio edit, a dev reload) takes effect at once. Removing the flag turns the tier off at once, with no restart.
12+
- **Fails closed.** An engine that cannot answer an object definition, an unknown object, or any value other than `true` leaves the tier off. Default OFF costs one in-memory registry lookup on a read that names a record. The business record is never probed for an object that declares nothing.
13+
- **Unchanged.** The rule itself: who counts as a reader, the read-only boundary, the request tables it covers on both doors, and the constructor option.

0 commit comments

Comments
 (0)