Skip to content

Commit 9bc6855

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21062-picker-queryable-key
2 parents 6dacc4c + f20f669 commit 9bc6855

56 files changed

Lines changed: 4217 additions & 508 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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
---
4+
5+
The shared engine case tables and the published contract suites in metadata-core no longer cite tracker numbers in their case labels; each label states its case in words
6+
7+
Clause-②: no
8+
9+
Several labels these tables and suites ship ended with an issue-tracker number where the case belonged. A
10+
test driven from them printed that number as part of its name, and a failing assertion quoted it as the
11+
reason. The number goes; where the label did not already say what the case is, it now does.
12+
13+
- `ENGINE_DELETE_DISPATCH_CASES`, `ENGINE_UPDATE_DISPATCH_CASES` and `ENGINE_FINDONE_PREDICATE_CASES`:
14+
the `what` labels of 22 rows. Among them, the compare-and-set rows now say the by-id path would drop the
15+
CAS guard; the payload-id rows say which declared `where.id` would be silently dropped; and the falsy
16+
`where.id` boundary says it is a scalar, so neither the different-row refusal nor the non-scalar refusal
17+
applies.
18+
- `@objectstack/metadata-core/testing`: the repository contract suite's `serialized-form identity` group
19+
title, and two `why` texts of `OBJECT_SCHEMA_MASK_CASES` (the empty-readable-set refusal, and the
20+
write-capable exemption, which now names the schema write gate, `manage_metadata`).
21+
22+
Text only: no case is added, removed or re-ordered, and no `options`, `data`, `expect`, `expectId`, `id`,
23+
`readable` or `context` value moves. A suite that selects or skips these cases by their label text (a
24+
`-t` filter, a skip list) needs the new spelling.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/service-knowledge': patch
3+
---
4+
5+
fix(service-knowledge): the realtime event bridge no longer keeps a `record.created` / `record.updated` / `record.deleted` branch, and its docs name the events ObjectQL really publishes (#20573)
6+
7+
No producer emits a bare `record.*` event: the ObjectQL engine publishes `data.record.created` / `data.record.updated` / `data.record.deleted` for a single-record write and `data.records.updated` / `data.records.deleted` for a predicate write (`multi: true`), and `@objectstack/spec` already dropped the bare names from `RealtimeEventType`. The `KnowledgeServicePlugin` subscription handler still carried a branch for them, with a `payload.record ?? payload` fallback for a shape nothing sends. That branch is removed. The `data.record.*` sync (record body from `after`, id from `recordId`) and the `data.records.*` stale-index warning are unchanged.
8+
9+
The `enableEventSync` option's TSDoc and the `handleRecordUpsert` / `handleRecordDelete` docs now name `data.record.created|updated|deleted` instead of `record.*`.
10+
11+
If a plugin of yours publishes a bare `record.created`, `record.updated` or `record.deleted` event through `IRealtimeService` and relied on the knowledge index following it, publish `data.record.created|updated|deleted` with the `DataEvent` payload instead (the record in `after`, its id in `recordId`).
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/service-analytics': minor
4+
'@objectstack/driver-sql': patch
5+
---
6+
7+
fix(service-analytics)!: the analytics read scope and the native `where` answer `$contains` / `$notContains` on a multi-valued or JSON-stored field by membership, with the one construct `driver-sql` emits, now exported from `@objectstack/core` (#20987)
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) a correction of which rows two analytics SQL faces answer for one operator on one declared field class: the read scope `compileScopedFilterToSql` compiles from a row policy, and the native strategy's rendering of a query's `where`. No authorable key, spelling, export or stored shape of metadata moves; `packages/spec` is untouched, and the contract sentence the faces now meet (`FILTER_OPERATORS.$contains`) is the one already declared. A policy or filter that was written stays written as it was, and what it now selects is what the data door already selected for it, so there is nothing a ledger entry could rewrite. The new refusal on a datasource whose SQL dialect the host cannot name is a refusal of a query, not of stored metadata. The other categories are closed on facts: the packages publish (not `unpublished`); no ADR-0087 id covers a filter operator's reading (not `registered` / `already-registered`); and the change is runtime behaviour plus one additive export, not a declaration change (not `runtime-interface-only` / `type-surface-only`). -->
12+
13+
**BREAKING**: this narrows what the analytics doors answer for one class of read. A row policy (the read scope the analytics plugin compiles from the security service, or a host's own `getReadScope`) whose `$contains` or `$notContains` names a field declared multi-valued (`multiple: true` on a multi-capable type, or a multi-option type) or JSON-stored now selects the rows holding the comparand as an ELEMENT of the stored list. It used to select every row whose stored JSON text contained the comparand as a substring, so on SQLite a policy could admit rows outside it, and on PostgreSQL every query under such a policy answered `500` (MySQL was not measured). An analytics count under such a policy now equals what the same caller reads through the data door. On a datasource whose SQL dialect the analytics host cannot name, such a policy now refuses the query (`READ_SCOPE_COMPILE_FAILED` / `500`) instead of falling back to the substring reading. It ships as `minor` under the launch-window convention.
14+
15+
**The `where`.** `POST /api/v1/analytics/query`, the dataset door and `/analytics/sql` on the native strategy render the same membership test for a `$contains` / `$notContains` in a query's `where` (or a dataset's `runtimeFilter`) on such a field: on PostgreSQL the query answers rows where it answered `500`, and on SQLite the count stops over-counting (`$contains`) and under-counting (`$notContains`). On a datasource whose dialect the host cannot name, the operator on such a field is refused `INVALID_FILTER` / `400`. The ObjectQL strategy already answered membership and is unchanged.
16+
17+
**Unchanged.** On a scalar text field `$contains` stays the substring test, on every face. `$notContains` keeps its NULL rule: a row with no value satisfies it. A host that wires no field metadata keeps the substring reading, because it cannot tell a JSON column from a text one; the analytics plugin wires it from the data engine.
18+
19+
**New export.** `@objectstack/core` exports `jsonMembershipPredicate(dialect, emitters, value)` and `jsonMembershipCandidates(value)`, with the `JsonMembershipDialect` and `JsonMembershipEmitters` types: the per-dialect membership construct (#17590) moved from `@objectstack/driver-sql`, where it was module-private, and made placeholder-agnostic. `@objectstack/driver-sql` imports it and emits byte-identical statements and bindings.
20+
21+
**What to do after upgrading.** Nothing, unless a policy or a dashboard filter relied on the substring reading of a multi-valued or JSON-stored field: such a filter now selects members only, as the data door always did. A host whose analytics `sqlDialect` hook answers nothing for a SQL datasource should answer `'sqlite'`, `'postgres'` or `'mysql'`, or the operator on such a field is refused.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `action:button` / `action:icon` refuse `endpoint` with the rename `ActionSchema` already prescribes — `endpoint` → `target` (#21005)
6+
7+
**BREAKING** — `endpoint` on an `action:button` or `action:icon` component (`ActionButtonProps`, `ActionIconProps`) is no longer a declared key. `ActionSchema` has always refused `endpoint` with "Did you mean `endpoint` → `target`?", while these two rows accepted it. objectui's console registers its own `api` handler, which reads `target` and never `endpoint`, so an `api` button written with `endpoint` passed the props gate and called nothing. The rows now refuse it with the same rename, read from the one alias table the action and both rows share. Write the endpoint as `target`.
8+
9+
Clause-②: yes (narrowing)
10+
11+
## FROM → TO
12+
13+
| you wrote (17.5 and earlier) | write instead |
14+
| --- | --- |
15+
| `{ type: 'action:button', properties: { actionType: 'api', endpoint: '/api/v1/x' } }` | `{ type: 'action:button', properties: { actionType: 'api', target: '/api/v1/x' } }` |
16+
| `{ type: 'action:icon', properties: { actionType: 'api', endpoint: '/api/v1/x' } }` | `{ type: 'action:icon', properties: { actionType: 'api', target: '/api/v1/x' } }` |
17+
| `endpoint` on a block with no `actionType` | add `actionType: 'api'` and rename `endpoint` to `target` |
18+
19+
**The one-line fix:** rename `endpoint` to `target` in the block's `properties`; the value (the URL the `api` action calls) is unchanged.
20+
21+
**What an author who still writes it sees.** A page is never refused for it: a page component's `properties` is an open bag, so `definePage()`, `defineStack({ pages })` and the page write door accept the page as before. `os validate` / `os build` / `os lint` report `component-props-unknown-key` as a warning at `properties.endpoint`, with the rename "Did you mean `endpoint` → `target`?" — the same clause `ActionSchema` prints. The two rows also stop answering `path` with the edit-distance guess `patch` (the declarative write's field values): `url`, `endpoint`, `path` and `href` all rename to `target`, on the action and on both blocks alike. A typed `ActionButtonProps` / `ActionIconProps` input fails `tsc` at `endpoint`.
22+
23+
## The migration kit
24+
25+
- **The D2 conversion `action-block-endpoint-to-target`** (protocol 18, retired from the load path) renames `endpoint` to `target` on an `action:button` / `action:icon` whose `actionType` is `api`, the one meaning the key declared, with one notice per block. It reaches blocks in regions, nested in a container's `children`, and in a slotted page's named slots, so a stored `page` row or a built artifact that carries the key loads with `target` through the rehydration seams, which replay it. An already-present `target` wins: a twin with the same value is dropped. A block with no `actionType`, another `actionType`, a non-string `endpoint`, or a `target` that names a different endpoint is left as stored and reported as a TODO. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.
26+
- **The D3 entry `action-block-endpoint-spelling-retired`** names what the rename cannot decide: the TODO sites above, and code — a custom action handler that read `endpoint` off the action reads nothing once the block carries `target`.
27+
- **No deprecation window**, per the project's startup-stage posture.
28+
29+
Census at landing: no producer in this repository (examples, templates, platform pages, fixtures) or in objectui's examples authors `endpoint` on either block. ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for.
30+
31+
<!-- adr-0087: registered action-block-endpoint-to-target, action-block-endpoint-spelling-retired -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the `object-kanban` `quickAdd` retirement no longer sends authors to the `kanban-ui` block, which objectui does not register
6+
7+
Clause-②: no
8+
9+
- The refusal of `quickAdd` on `object-kanban` now ends "Delete the key; `object-kanban` offers no quick-add control." It used to say the control "is unchanged on the `kanban-ui` block". objectui retired that block (objectui#8257), so a node of that type saves clean and renders nothing. The refusal itself is unchanged: the same key is still refused, with the same code and path.
10+
- The same sentence replaces the old one in the `os migrate meta --from 17` output (the `object-kanban-quick-add-retired` entry) and in the summary of the `object-kanban-quick-add-removed` conversion. That entry no longer offers "move the board to a host that renders the `kanban-ui` block" as a second way out.
11+
- No author action beyond the existing one. `quickAdd: true` on an `object-kanban` is still a parse error. Delete the key.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/cli': patch
3+
'@objectstack/runtime': minor
4+
---
5+
6+
fix(cli): `os migrate plan` / `apply` no longer run the app's `onEnable` or a host plugin's post-declaration hooks during their boot
7+
8+
Clause-②: yes (widening)
9+
10+
The two schema commands boot the host's stack to read what it declares. That boot ran the
11+
config's `onEnable`, and every `kernel:bootstrapped` / `kernel:listening` hook a host plugin
12+
registered from `init()`. A hook that reads a table the plan does not declare then failed on
13+
every plan. On `examples/app-crm`, whose `onEnable` binds positions to permission sets, each
14+
plan printed six `[sql-driver] DATABASE_ERROR` lines and six `position binding lookup failed`
15+
warnings, on a database `apply` had just migrated as well as on an absent one.
16+
17+
The boot now composes host code for its declarations only:
18+
19+
- `AppPlugin` takes a new `skipOnEnable` option. When it is set, `start()` does not run the
20+
bundle's `onEnable`, logs that it withheld it, and reports it through `onEnableWithheld`. The
21+
migrate commands set it on the app they compose from `objectstack.config.ts`.
22+
- A host plugin's `init()` gets a context that does not register `kernel:bootstrapped` or
23+
`kernel:listening` hooks. The kernel contract defines those phases as work after registration
24+
ends: reconcile/backfill, and opening listeners. `kernel:ready` hooks still run, and the
25+
write guard still refuses their row writes. `kernel:shutdown` hooks and data hooks register
26+
as before.
27+
- The plan's notes, and the `--json` payload's `composition.notes`, carry one line naming what
28+
was not run.
29+
30+
The plan itself is unchanged: the same tables, the same pending DDL, the same drift. `apply`
31+
still flushes the DDL the operator confirms and still runs the coverage pass. The platform's own
32+
plugins are untouched, so the value-shape gate announcement still prints.
33+
34+
`@objectstack/runtime` widens its public surface, additively: `AppPlugin`, exported from the
35+
package root, gains the optional constructor option `skipOnEnable` (default `false`) and the
36+
read-only getter `onEnableWithheld`. A composition that does not pass the option gets exactly
37+
the behaviour it had, `onEnable` included.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
docs(spec)!: the security service contract's field answers for a caller who resolves no permission set exclude the fields that declare `requiredPermissions` (#21063)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) Only the prose of the ISecurityService interface moves: no method, parameter, return type, schema key or stored shape changes, so objectstack migrate meta has nothing to rewrite. The answers it now states are the ones every field-level requiredPermissions declaration already implied. -->
10+
11+
**BREAKING for implementers and consumers of `ISecurityService` field answers.**
12+
13+
**What changed.** The contract in `@objectstack/spec/contracts` now states the
14+
field answers for a non-system caller who resolves no permission set. Such a
15+
caller holds no permission-set field grant and no capability. No grant narrows
16+
its answers, and a field's own declarations still apply: a field that declares
17+
`requiredPermissions` is not in its `getReadableFields` answer (unless a
18+
`maskingRule` on the field serves it masked, which keeps it as a served
19+
column), and it is not in its `getWritableFields` answer.
20+
`getMetadataReadableFields` answers the same for that caller when the
21+
deployment's fallback set resolves to nothing. The contract used to say the
22+
data-plane answer for that caller was the full field set, because the engine
23+
middleware skipped its whole field gate for it. The middleware skips only its
24+
permission-set grant gates.
25+
26+
**Who this reaches.** An implementation of `ISecurityService` must answer this
27+
way for that caller. A consumer that relied on the full field set for that
28+
caller now receives the narrower answer from the reference implementation
29+
(`@objectstack/plugin-security`).
30+
31+
**What to do.** An implementation folds each field's `requiredPermissions`
32+
into its answer for this caller exactly as it does for a caller whose
33+
permission sets lack the capability. A consumer needs no change.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
---
4+
5+
fix(plugin-security)!: a field that declares `requiredPermissions` is not served to a caller who resolves no permission set, and that caller may not query on it or write it (#21063)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author wrote is rewritten or changes meaning: every field-level requiredPermissions already declared mask on read and deny on write for a caller who does not hold all of it, and this change makes the runtime honour that declaration for the one caller class it skipped. No stored metadata, schema key or published type moves. -->
10+
11+
**BREAKING for callers who resolve no permission set.**
12+
13+
**What changed.** A field that declares `requiredPermissions` is masked on read
14+
and denied on write unless the caller holds all of them (ADR-0066 D3). A caller
15+
who carries a principal but resolves no permission set holds no capability, so
16+
the gate applies to it, but the runtime served that caller the stored value,
17+
let it filter, sort, group and aggregate on the field, and accepted a write
18+
that named it. The explain engine already reported the field hidden for that
19+
caller. Now the field is not served to it (a field that also declares a
20+
`maskingRule` is served masked, as before). A filter, sort key, group key or
21+
aggregate that names the field is refused with `403 PERMISSION_DENIED`, and so
22+
is a write payload that names it, as for any other caller who lacks the
23+
capability.
24+
25+
The published field answers agree with what is served and refused.
26+
`ISecurityService.getReadableFields`, `getQueryableFields` and
27+
`getWritableFields` no longer list such a field for this caller, and neither
28+
does `getMetadataReadableFields` when the deployment's fallback set resolves to
29+
nothing. The write preview answers such a payload as the write path does.
30+
31+
**Who this reaches.** A caller who resolves no permission set but carries a
32+
position, a named permission set or a user id. A caller with none of the three
33+
is handed through untouched, as before, and the field projections say so. A
34+
caller who resolves at least one permission set is unaffected, and so is a
35+
system context. Whether this caller may read or write the object at all is
36+
unchanged.
37+
38+
**What to do.** Nothing, unless such a caller needs the field. A field's
39+
`requiredPermissions` name the capabilities that open it, so give the caller a
40+
permission set that holds all of them, or drop the requirement from the field.

0 commit comments

Comments
 (0)