Skip to content

Commit 323ab0b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21803-artifact-lock-package-axis
2 parents f387625 + 8832655 commit 323ab0b

53 files changed

Lines changed: 1681 additions & 204 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): an inline `object-form` field declares the `grid` widget's eight camelCase field-level keys, and each snake_case spelling is refused naming its camelCase key (#21768)
6+
7+
Clause-②: yes (widening)
8+
9+
A widening of a published authoring surface: every value that parsed before still parses, and eight keys that were refused now parse. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`.
10+
11+
**`@objectstack/spec`**
12+
13+
- **The runtime form field takes the `grid` widget's field-level keys.** An `object-form` `customFields` member, and the inline entry of an `object-form` or `object-master-detail-form` section's `fields`, now declare `minRows` and `maxRows` (numbers), `allowAdd`, `allowDelete` and `allowReorder` (booleans, on unless `false`), and `totalField`, `addLabel` and `sortField` (strings). These are the value types objectui's `GridFieldMetadata` declares. The `grid` widget reads each one off a `type: 'grid'` field: `minRows` stops Remove, `maxRows` stops Add, Duplicate and the blank entry row, `addLabel` labels the Add button, and `sortField` names the row field the grid stamps with each row's index, so a drag-reorder is saved. objectui renamed the eight from snake_case to camelCase, with no dual read, and the `.objectui-sha` pin `9dfaca654311` carries that rename.
14+
- **`totalField` here is the CHILD column the grid sums into its footer.** On `record:line_items` and on an `object-master-detail-form` detail entry, the same spelling names the PARENT field the sum is saved to, and their child column is `amountField`. The describe states the difference. The other blocks' keys are unchanged.
15+
- **The snake_case spellings are still refused, and each refusal now names its own replacement.** `min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label` and `sort_field` are each answered with "Rename the key to `minRows`" (and so on); the value stays the same. The old answer said the keys would come in once the widget read a camelCase spelling, and the widget now does. Two retired spellings on one field get one line each.
16+
- **`record:line_items`' `sortField` refusal** now says no block takes an authored `sortField` *for child records*. An inline `grid` field takes one for the rows of its own value, so the unqualified sentence was no longer true.
17+
18+
## FROM → TO
19+
20+
| you wrote | write instead |
21+
|:--|:--|
22+
| `customFields: [{ name: 'items', type: 'grid', min_rows: 1, allow_add: false }]` | `customFields: [{ name: 'items', type: 'grid', minRows: 1, allowAdd: false }]` |
23+
| `total_field: 'amount'` on an inline grid field | `totalField: 'amount'`, naming the child column summed |
24+
| `add_label: 'Add line'`, `sort_field: 'position'` | `addLabel: 'Add line'`, `sortField: 'position'` |
25+
26+
The one-line fix: rename each key to its camelCase spelling, keeping its value. Nothing that parsed before is refused, so no ADR-0087 conversion or D3 entry is owed.
27+
28+
## Who is affected, measured
29+
30+
- **objectstack** at `75ddcd1b41` (this branch's base): no writer of either spelling on an inline form field in `examples/`, `skills/`, `content/docs/` or `apps/`. The spec's own pin is the only one: its `min_rows` refusal probe.
31+
- **objectui** at the pin `9dfaca654311`: the camelCase writer is the schema catalog's `fields-grid/line-items-grid` example, a `grid` field carrying all eight keys, and it parses. No production source reads a snake_case spelling. The only snake_case occurrences left are objectui's own refusal faces: the TS tombstones, the zod alias refusals, and the widget's refusal.
32+
- **hotcrm**, **cloud** and deployed metadata were not measured.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
"@objectstack/spec": patch
4+
---
5+
6+
fix(plugin-security)!: on the write doors, a row the caller cannot read answers what a nonexistent id answers
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: registered by-id-write-unreadable-row-not-found -->
11+
12+
**BREAKING**: a by-id update or delete of a row the caller cannot read now answers `404 RECORD_NOT_FOUND`, with exactly the body an id that names no row gets, for every principal class. On the write doors, "hidden" and "gone" are now one answer to a caller who cannot read the row. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new.
13+
14+
**What changed.** The answer used to depend on which gate saw the row first. Where a write-class row filter binds the caller, the by-id write pre-image check answered `403 PERMISSION_DENIED`. Where none binds it, a later gate answered with its own 403: `FORBIDDEN` from record sharing, or a parent-derived gate's code on attachments and comments. Meanwhile a nonexistent id answered `404`. So the write door could tell a hidden row apart from a missing one. The pre-image check now asks the read door's own question first, for the by-id write the caller addressed: a by-id read in the caller's context, every data middleware's visibility included. A row that read does not return gets the read door's not-found producer. A store fault propagates as raised, and a read-time policy refusal is not treated as absence.
15+
16+
**What is refused now that was not.** A principal that no write-class row filter binds could have its by-id write admitted on a row the read door hides from it. One example is the uploader of an attachment, or the author of a comment, whose parent record they can no longer read. That write is now refused with the not-found answer, as it already was for every principal a row filter binds.
17+
18+
**FROM → TO.** A by-id update or delete of a row hidden from the caller: FROM a `403` (`PERMISSION_DENIED`, `FORBIDDEN`, or a parent-derived gate's code) → TO `404 RECORD_NOT_FOUND`, the body a nonexistent id gets.
19+
20+
**If you are affected.** A client that read a by-id write's `403` as "the row exists, but you may not change it" should read `404 RECORD_NOT_FOUND` the way the read door means it: no row you can see has this id.
21+
22+
**Unchanged.**
23+
- A caller who can read the row but may not write it keeps its 403. They already see the row.
24+
- By-id writes the platform issues under the caller's context keep their previous answer, because the caller never named their target: the engine's cascade delete of a dependent row, a hook's write, and the referential clear of a lookup.
25+
- Writes that are not routed by id are unchanged.
26+
27+
`security/explain` follows enforcement. Its record verdict for an update or delete of a record the principal cannot read is now the missing-record shape: `visible: false`, with no decider.

‎.changeset/console-0abd4f9f8769.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/console": minor
3+
---
4+
5+
Console (objectui) refreshed to `0abd4f9f8769`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 3 releasing of 5 changesets added across 5 non-merge commits; omitted: 2 release-nothing changesets (they ship no package code).
8+
9+
- **minor** — `@object-ui/types` declares each renderer's NODE SLOTS once (`NODE_SLOT_DECLARATIONS`, `nodeSlotsFor`), and `objectui check`, core `validateSchema`, the SDUI parser's `validateTree` and the `kind:'html'` page compile walk those slots as well as `children` (objectui#11170). Its changeset declares `Clause-②: yes (narrowing)`: a node under a slot that was never judged is judged now. (objectui `c4c506b9e`)
10+
- **minor** — The screen-flow runner names the flow by its label, in the user's language (objectui#11092, the objectui half of objectstack#20318). (objectui `39a3e91fa`)
11+
- **patch** — The External Datasource panel in Setup and Studio reads the `{ success, data }` envelope its routes answer (objectui#11628). On a federated datasource such as the showcase's `show… (objectui `0abd4f9f8`)
12+
13+
No objectui commit in the range carries `!`, and no changeset in it declares `major` or carries the breaking annotation. objectui#11170's narrowing is objectui's own validation reach and changes no ObjectStack-authorable key. The manifest this repository ships is generated without `slotsFor`, so its entries carry no slot list. The release-nothing pair is objectui#11396, which derives `MasterDetailDetailConfig` from the spec's `details` entry by reference, member for member the same, and objectui#11095, a KPI-tile invalidation pin.
14+
15+
objectui range: `9dfaca654311...0abd4f9f8769`
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `0abd4f9f8769`.
6+
7+
Clause-②: no
8+
9+
Every anchor was mapped through the objectui diff `9dfaca654311..0abd4f9f8769`, 50 paths over five commits. None of those paths is an objectui file that an asserting record cites, so every cited file is byte-identical across the hop (`git diff --quiet`) and every anchor held unmoved. The seven quoted anchor lines verify against objectui at the new pin. Three records carry a count, and each count was re-taken by its record's own method with the same reading: the `keyboardNavigation` hit lines (15, against 3 for the `schema.editable` control), `ObjectKanban.tsx`'s `quickAdd` / `onQuickAdd` (2 each, against 11 for `onCardClick`), and the `ElementDataSourceGate` occurrences in five `src/index.tsx` shells (0, 3, 3, 3 and 4).
10+
11+
The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that first reproduced every `9dfaca654311` number. The corpus is now 7650 tracked files. All 98 checked tokens read as before: every zero still reads zero, and `Span` / `SpanSchema` still read 508 / 57.
12+
13+
No key, default, enum member or export moves.

‎.objectui-sha‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9dfaca654311cddd81714153c4f82c241d7cdc54
1+
0abd4f9f8769fc4c19ad2f96707684876f74c09f

‎content/docs/permissions/attachments-access.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ editable by design). A multi-delete requires *every* matched row to pass.
7171
| Code | Status | When |
7272
| --- | --- | --- |
7373
| `ATTACHMENT_DELETE_DENIED` | 403 | The caller can read the attachment, but is neither the uploader nor able to edit the parent record |
74-
| `PERMISSION_DENIED` | 403 | The caller cannot read the parent record, so the attachment is not visible to them. This is the platform's not-visible refusal whichever layer gives it — the row-level write check that runs before the gate, or the gate itself for a caller that check does not cover — and it names neither the parent nor the attachment's link to it |
74+
| `RECORD_NOT_FOUND` | 404 | A delete or update **by id** of an attachment the caller cannot read — its parent record is not readable to them. On the write doors a row the caller cannot read is a row that does not exist: the answer is exactly what an id that names no attachment gets, for every caller, so a hidden attachment and a missing one cannot be told apart. It applies even to the uploader once the parent is out of their sight, because the read door no longer returns the attachment to them either |
75+
| `PERMISSION_DENIED` | 403 | The gate's own not-visible refusal, where the gate answers before that check. It names neither the parent nor the attachment's link to it |
7576

7677
An update of another user's attachment follows the same rule — the uploader or
77-
a parent editor — and a caller who cannot read the parent gets the same
78-
not-visible refusal.
78+
a parent editor — and a by-id update of an attachment the caller cannot read
79+
gets the same not-found answer.
7980

8081
The platform baseline also ships a parent-blind row-level delete floor
8182
(`owner_only_deletes`: you may delete only the rows you created, for members

‎content/docs/permissions/permissions-matrix.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ flowchart TD
360360
| 7 | **Field-Level Security** | Which fields is the user allowed to see/edit? |
361361

362362
<Callout type="tip">
363-
**Performance:** For reads, steps 2–6 are compiled into a query filter (owner-match ∪ materialized shares, AND-ed with RLS) at query time, not evaluated record-by-record. By-id writes are verified with a pre-image check: the target row is re-read through the write-scope filter before the mutation. This keeps security checks efficient even on tables with millions of rows.
363+
**Performance:** For reads, steps 2–6 are compiled into a query filter (owner-match ∪ materialized shares, AND-ed with RLS) at query time, not evaluated record-by-record. By-id writes are verified with a pre-image check: the target row is re-read through the write-scope filter before the mutation, and a row the caller cannot read answers exactly what a missing id answers (`404 RECORD_NOT_FOUND`). This keeps security checks efficient even on tables with millions of rows.
364364
</Callout>
365365

366366
## See also

‎content/docs/protocol/kernel/error-handling.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,12 @@ Content-Type: application/json
907907
}
908908
```
909909
910+
⚠️ **This is the answer for a row the caller can read.** A by-id update or delete of a row
911+
the caller **cannot read** answers exactly what an id that names no row answers —
912+
`404 RECORD_NOT_FOUND` — whichever rule hides the row, so the write door never tells
913+
"hidden" apart from "gone". A caller who can read the row but may not write it gets the 403
914+
shown here.
915+
910916
⚠️ **`FORBIDDEN`, not `PERMISSION_DENIED`.** A by-id write the sharing rules refuse carries
911917
`FORBIDDEN`; `PERMISSION_DENIED` is what the capability and identity guards carry. Both are
912918
403s in the same envelope on this door, so branch on either — ⛔ but do not assume one code

‎content/docs/references/ui/component.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,14 @@ Sort field and direction pair
657657
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | The value type a formula field displays (number / text / boolean / date) |
658658
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | The roll-up a summary field displays — the object field's own `{ object, field, function, … }` |
659659
| **columns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | optional | The columns of a `grid` field — the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes |
660+
| **minRows** | `number` | optional | A `grid` field's minimum row count: its Remove action is disabled at this many rows (no minimum when unset). Read only by the `grid` widget |
661+
| **maxRows** | `number` | optional | A `grid` field's maximum row count: its Add and Duplicate actions are disabled, and no blank entry row is drawn, at this many rows (no maximum when unset). Read only by the `grid` widget |
662+
| **allowAdd** | `boolean` | optional | Whether a `grid` field offers Add, and each row's Duplicate (a duplicate is an add): on unless `false`. A read-only or disabled grid offers neither. Read only by the `grid` widget |
663+
| **allowDelete** | `boolean` | optional | Whether a `grid` field offers each row's Delete: on unless `false`. A read-only or disabled grid never offers it. Read only by the `grid` widget |
664+
| **allowReorder** | `boolean` | optional | Whether a `grid` field's rows can be reordered by dragging: on unless `false`. A read-only or disabled grid never offers it. Read only by the `grid` widget |
665+
| **totalField** | `string` | optional | The CHILD column a `grid` field sums into its footer total — the `name` of one of its `columns`; no total shows when unset. Not the PARENT field a master-detail or `record:line_items` sum is saved to: those blocks spell that `totalField` and the child column `amountField`, and the grid writes no parent field. Read only by the `grid` widget |
666+
| **addLabel** | `string` | optional | Label of a `grid` field's Add button, also named in its empty state (the locale's own wording when unset). A plain string. Read only by the `grid` widget |
667+
| **sortField** | `string` | optional | A field on each row that a `grid` field stamps with the row's index (0, 1, 2, …) on every change, so the order a drag-reorder leaves is saved with the rows (rows carry no position when unset). A row field, not one of `columns`: a column of that name has its typed value overwritten. Read only by the `grid` widget |
660668

661669
### Nested Shape: `ObjectFormProps.sections[number]`
662670

0 commit comments

Comments
 (0)