Skip to content

Commit b6d58dd

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21127-connector-source-live-row
2 parents 7bc7c5c + fde553c commit b6d58dd

85 files changed

Lines changed: 5773 additions & 486 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.

‎.changeset/20281-connector-sync-moved-to-mapping.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@ declared-connector item carried neither key, and the def a provider registers is
2525
own. The `latest_wins` and `soft_delete` defaults read as configured policy and did
2626
nothing; the platform has no soft delete.
2727

28-
**Added (declared, not yet executed):** `mapping.connectorSource` — the pull
28+
**Added:** `mapping.connectorSource` — the pull
2929
binding on the target side, beside the mapping's existing `targetObject`,
3030
`fieldMapping`, `mode` and `upsertKey`: `connector` (a `rest` or `openapi`
3131
connector instance), `action` (the action that reads the records), optional
3232
`input`, optional `recordsPath`, and an optional `watermark` (`field` on the
3333
record, `param` on the request) for a timestamp-incremental pull. Version 1 is a
3434
one-way pull. It carries no cadence (a `job` sets that), no credential (the
35-
connector instance holds it) and no delete or conflict policy. Nothing executes it
36-
in this release, and `os validate` / `os build` warn when it is authored.
35+
connector instance holds it) and no delete or conflict policy. The connector sync
36+
executor, `@objectstack/service-automation`'s `pullConnectorSource` (#20919), reads
37+
it; nothing schedules a pull until the `job` stage lands.
3738

3839
### FROM → TO
3940

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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/service-analytics': patch
3+
---
4+
5+
fix(service-analytics): the analytics field-level read gate refuses a cube member whose `sql` names no field, instead of letting the query run (#20965)
6+
7+
Clause-②: no
8+
9+
**What changed.** Where the analytics field-level read gate judges a cube's
10+
object (a security service is registered and gives a field answer for that
11+
object), a query that names a cube member whose `sql` is neither a column
12+
reference (a field of the cube's object, or a relationship path ending in one)
13+
nor `'*'` is now refused `403 PERMISSION_DENIED` on
14+
`POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql`, before either
15+
strategy runs. Whatever
16+
the caller may read, the member is refused. That covers an expression member
17+
of a cube that reached the service without the spec's parse (the cube
18+
registry never parses: `analyticsCubes` and `AnalyticsServicePlugin({ cubes })`
19+
arrive as written), a declared member with no `sql` string, and a member the
20+
query names itself that is not a column reference. The gate used to stand down
21+
on such a member, because it names no field, and the native-SQL strategy then
22+
compiled it into its statement as written: a read of fields no permission
23+
verdict was reached for. The refusal names the member and the object, and
24+
never the member's `sql`.
25+
26+
**What is not affected.** A member that is a column reference is judged by the
27+
field it resolves to, as before. A `count` over `'*'` names no field and is
28+
served. A deployment with no security service, and an object the security
29+
service gives no field answer for, apply no field-level check, as before. The
30+
spec's parse already refuses an expression member, so a cube that parses is
31+
unaffected.
32+
33+
**If a widget stopped answering,** its cube carries an expression member from
34+
before the parse refused one. Re-author the member as a column reference, or
35+
declare the derived value on an ADR-0021 dataset: a conditional count or sum
36+
is a dataset measure with its own `filter`, and a ratio of measures is
37+
`derived: { op: 'ratio', of: [...] }`.
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/runtime': patch
4+
'@objectstack/metadata-protocol': patch
5+
---
6+
7+
feat(spec): discovery reports which optional `/auth` route families are mounted, starting with the better-auth admin family (`authFamilies.admin`) (#21046)
8+
9+
Clause-②: yes
10+
11+
**New key.** `DiscoverySchema` declares an optional `authFamilies` block, `{ admin: boolean }`. `admin` says whether the better-auth admin family (`{routes.auth}/admin/*`: `list-users`, `set-role`, `update-user`, `ban-user`, …) is mounted on this deployment. On a deployment that does not enable the admin plugin those routes answer a plain `404`, the same as a mistyped path, so a caller checks `authFamilies.admin` before building a URL into the family. `@objectstack/spec/api` also exports the block's schema (`AuthFamiliesSchema`, type `AuthFamilies`) and its reader, `readAuthFamilies(authService)`.
12+
13+
**Same answer as `/auth/config`.** The value is the auth service's own `getPublicConfig().features.admin`, the object `GET /api/v1/auth/config` serves. Both discovery producers read it through `readAuthFamilies`: `getDiscovery()` in `@objectstack/metadata-protocol` (served by `@objectstack/rest` at `GET /api/v1/discovery`) and `getDiscoveryInfo()` in `@objectstack/runtime` (served at `GET /.well-known/objectstack`). Neither re-derives whether the admin plugin is on, so on one boot the two documents and `/auth/config` agree. On a stock boot `authFamilies.admin` is `false`. With the admin plugin on (`plugins.admin: true`, or SCIM, which forces it on) it is `true`.
14+
15+
**When the key is absent.** A producer that cannot read the answer emits no `authFamilies`, rather than a guessed `false`. That happens when no `auth` service is registered (then `routes.auth` is absent too), when the registered service has no `getPublicConfig()`, or when that call throws (`/auth/config` answers `500 AUTH_CONFIG_ERROR` in that state). Treat an absent block as "not known to be mounted".
16+
17+
**What did not change.** No existing key, route or status moved. The unmounted admin routes still answer a plain `404`.
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.

0 commit comments

Comments
 (0)