Skip to content

Commit 494acb0

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21220-dataset-field-column-reference
2 parents 0d5e446 + ef96c9e commit 494acb0

87 files changed

Lines changed: 2854 additions & 409 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
Provenance comments in `@objectstack/metadata-protocol` cite the commits that decided them, not tracker numbers that no longer resolve
6+
7+
Clause-②: no
8+
9+
Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub.
10+
Each one now cites the commit in this repository's history that made the decision it describes
11+
(and ADR-0005's design-principle-3 correction where that record exists). Some of these docblocks sit
12+
on exported members, so the reworded text appears in the published `index.d.ts` / `index.d.cts`, and
13+
a few comments that esbuild keeps appear in the JavaScript output.
14+
15+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/verify': patch
3+
'@objectstack/plugin-dev': patch
4+
'@objectstack/plugin-hono-server': patch
5+
---
6+
7+
The `verify --rls` report and messages, the dev plugin's tenancy and no-auth messages and the Hono server's no-API warning no longer cite tracker numbers; each one states the decision behind it in words
8+
9+
Clause-②: no
10+
11+
Strings these three packages show to operators, and print in verification reports, sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
12+
13+
- `@objectstack/verify`, the `objectstack verify --rls` report: the header reads `=== objectstack verify (RLS / cross-owner by-id-write invariant) — <app> ===`, and the position-persona line reads `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed`.
14+
- `@objectstack/verify`, an `rls-hole` verdict's detail: it says the by-id write bypassed RLS, and that a caller that cannot read a record must not be able to write it.
15+
- `@objectstack/verify`, the refusal when the RLS probe persona cannot be provisioned (no ObjectQL engine): it says a by-id write that bypasses RLS is what becomes unreachable. The matching position-persona refusal drops its citation.
16+
- `@objectstack/verify`, the records the probe writes: the probe permission set's row-level-security policy description, the probe `sys_permission_set` row's description and the position persona's `sys_user_position` reason drop their citations. Each already said what it is for.
17+
- `@objectstack/verify`, the `bootStack` refusal for `multiTenant: true` when the app does not declare `@objectstack/organizations`: the citation beside "a package merely reachable through NODE_PATH or a hoisted workspace store is not accepted" goes.
18+
- `@objectstack/plugin-dev`, the `REST API NOT enabled` warning for a stack that mounts no auth: it says anonymous access to object data is always denied, with no setting that turns that off.
19+
- `@objectstack/plugin-dev`, the two refusals for an `OrganizationsPlugin` that refused to be constructed or failed to initialize: they drop their citations. Each already says `OS_ALLOW_DEGRADED_TENANCY` covers only an absent multi-org runtime, not a present one that declined.
20+
- `@objectstack/plugin-hono-server`, the boot warning for a server with no data or discovery API mounted: it drops its citation. It already says the plugin is a transport adapter that serves neither.
21+
22+
Text only: no status, error code, exit code, route, field, export, verdict or count moves. A log filter or script that matched the old text (for example the report header's `RLS / #NNNN` spelling) needs the new spelling.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): a row-level `check` judges a `date`, `datetime` or `time` column as the row will be stored, so the write and the read the same policy scopes give one answer for one row (#21109)
6+
7+
Clause-②: no
8+
9+
The write check evaluates the compiled `check` filter in-process against the write's post-image. That image held each value as the caller or a hook wrote it, while the read compares the value the driver stored, in its column's storage form, against a comparand put into the same form. On a temporal column the two forms differ, so one row could get two answers. Measured through `ObjectQL.insert` with `SecurityPlugin` on SQLite, as a member resolving a permission set, with the same predicate as `using` and `check`:
10+
11+
| `check` | written | write, before | stored | read |
12+
|---|---|---|---|---|
13+
| `record.due_on == '2026-01-05'` | `'2026-01-05T15:00:00Z'`, or a `Date` on that day | 403 | `2026-01-05` | shown |
14+
| `record.start_time == '09:00'` | `'09:00:00'` | 403 | `09:00:00` | shown |
15+
| `record.due_at == '2026-01-05T10:00:00Z'` | `'2026-01-05T18:00:00+08:00'` | 403 | `2026-01-05T10:00:00.000Z` | shown |
16+
| `record.due_on > '2026-01-05'` | `'2026-01-05T15:00:00Z'` | admitted | `2026-01-05` | hidden |
17+
18+
Now, before the check is judged, every column the object declares `date`, `datetime` or `time` is put into `@objectstack/core`'s `temporalStorageForm`, the rule the drivers write and compare those columns by. That applies to the post-image's value and to the check's value comparands on the column (`$eq`, `$ne`, the orderings, `$in`, `$nin`, `$between`). The first three rows above are now admitted. The last is now refused, which is the read/write agreement this change buys: the read never showed that row, so a write that stores a day the predicate excludes is no longer admitted on the strength of the time of day it was sent with. Every insert, by-id update and predicate update takes the same step.
19+
20+
Unchanged: a column the object does not declare temporal is judged as written, whatever its value looks like; an object whose schema cannot be loaded is judged as before; a value the storage rule cannot read is judged as written; and refusals keep their code and status (`PERMISSION_DENIED` / 403).
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
---
4+
5+
fix(service-analytics)!: a cube measure whose `sql` is a relationship path (`account.name`) is judged by the aggregate × field-type table, described in `fields[]` and presented on the native-SQL strategy by the declaration on the object the path's last hop reaches, as a measure over the cube's own column already was
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered dataset-measure-selecting-aggregate-field-type-refused, dataset-measure-aggregate-field-type-refused) the pairs this change refuses are exactly the pairs AGGREGATE_FIELD_TYPE_COMPATIBILITY already refuses, and the table is not edited: every refused min / max pair is registered under protocol major 18 by the first id and every refused sum / avg pair by the second, each with its routes. This change makes the cube door read a relationship-path column's declaration on the object the path reaches, where it already read a base-object column's; it refuses a query shape, not a stored one, and no authorable key, export or stored row moves. -->
10+
11+
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept on the native-SQL strategy, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
The column a relationship path names is located by the one hop resolver both strategies join and read it through: the cube's declared join at that path, else the relationship field's declared `reference`.
14+
15+
FROM → TO, for a `measures` entry that resolves to a cube measure over a relationship path (an authored cube measure, or a compiled dataset's measure over an `include`d relationship):
16+
17+
- `min` / `max` / `sum` / `avg` over a related field of a type the table refuses for that aggregate (the string family such as `text`, `select`, `lookup`; the JSON-stored, file and `formula` types; and the rest the table lists): FROM, on the native-SQL strategy, `200` with the related column's own value (a string such as `"zeta"`) under `fields[] { type: 'number' }` on SQLite and PostgreSQL, and for `sum` a plausible `0` on SQLite and `500 DATABASE_ERROR` on PostgreSQL; the ObjectQL strategy refused it as a cross-object measure. TO `400 INVALID_FIELD` on both strategies, before either reads anything — the refusal a base-object column of the same type already got.
18+
- `min` / `max` over a related numeric field on PostgreSQL: FROM the exact-decimal string (`"250.000000000000000000000000000000"`) under `fields[] number`. TO the number `250`.
19+
- `min` / `max` over a related `date`, `datetime` or `time` field: FROM `fields[] { type: 'number' }` beside the instant. TO `fields[] { type: 'time' }`.
20+
21+
**What an author sees now.** `400 INVALID_FIELD`, naming the measure as the request wrote it, the cube, the path, the related object and the type it declares, saying the query was not run, and naming the types the aggregate accepts. The thrown error carries `member`, `param` (`measures`), `cube`, `field` (the path, `account.name`) and `object` (the related object that declares the column).
22+
23+
**What to write instead.** Aggregate a related field of a type the aggregate accepts, or `count` the rows. A first or last related record by a text value is a sort on a list, not an aggregate.
24+
25+
**Who is affected.** A dashboard, report or caller that asked `min` / `max` / `sum` / `avg` of such a related column through the native-SQL strategy and read the answer as a real one. No example app and no shipped cube or dataset authors such a pair. A dataset whose measure aggregates such a related field is now refused when its query runs (`INVALID_FIELD`), where its compile check, which reads the base object's declaration, still lets it through.
26+
27+
**Unchanged.** Every pair the table accepts; a measure over the cube's own column; `count`, and `count_distinct`, which keeps its own door; a related column the host's field metadata cannot describe; an expression `sql` or `*`; a host that wires no `sourceFieldMeta`; and the ObjectQL strategy's refusal of a related-field measure the table accepts (`max` over a related `number`), a capability limit of the engine aggregate — run that query on a native-SQL driver.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
'@objectstack/driver-sql': minor
3+
'@objectstack/driver-turso': minor
4+
'@objectstack/spec': patch
5+
---
6+
7+
fix(driver-sql,driver-turso)!: an upsert whose conflict lands on another organization's row is refused with `UNIQUE_VIOLATION` and writes nothing, and an upsert never changes a row's organization (#21185)
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: registered driver-upsert-cross-organization-conflict-refused -->
12+
13+
**BREAKING for `upsert` callers on the SQL drivers and on `TursoDriver`.**
14+
15+
**What changed.** `upsert` resolves its conflict against the whole table, and the
16+
primary key and a `unique: 'global'` column are installation-wide, so the row a
17+
tenant-scoped call (`options.tenantId` on an object with a tenant column) collided
18+
with could belong to another organization. The merge wrote the payload onto that
19+
row, tenant column included. Now:
20+
21+
- **A tenant-scoped upsert merges only into a row of the organization the row is
22+
written under**, for any conflict target, the primary key included. A conflict
23+
that lands on a row of another organization, or on a row with no organization,
24+
is refused with `code: 'UNIQUE_VIOLATION'`, `status: 409`, and nothing is
25+
written. That is the answer `create()` gets for the same collision: from the
26+
caller's organization the call is an insert, and that insert collides. The
27+
refusal names no organization and no value of the row it collided with.
28+
- **The tenant column is insert-only** (`insertOnlyUpsertColumns`), like `id`,
29+
`created_at` and `auto_number` columns: an upsert with no tenant context merges
30+
into the row it lands on and keeps that row's organization.
31+
32+
Mechanism, per face: on SQLite, PostgreSQL and the remote (libSQL) face, the merge
33+
statement carries the organization predicate (`DO UPDATE … WHERE`), so another
34+
organization's row is never written. On MySQL, whose `ON DUPLICATE KEY UPDATE`
35+
takes no `WHERE`, the statement and a read of the landed row run in one
36+
transaction (a savepoint inside a caller's transaction), and the read's failure
37+
rolls the write back. The remote face now also stamps the caller's organization on
38+
the row it inserts, as the local faces do.
39+
40+
## FROM → TO
41+
42+
| you relied on | now |
43+
|:--|:--|
44+
| a tenant-scoped `upsert` merging into a row of another organization | refused with `UNIQUE_VIOLATION` / 409, nothing written |
45+
| an `upsert` payload's tenant value moving the row it merges into | the row keeps its organization; to move a row between organizations, use `update()` |
46+
47+
**What is not affected.** A tenant-scoped upsert whose conflict lands on a row of
48+
its own organization merges as before, on every target. An upsert that inserts
49+
lands under the caller's organization, or under the organization the payload
50+
names explicitly, as before.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the `filter-text-operator-declared-type-refused` migration entry's control no longer counts JSON-stored fields, and it names the separate door that refuses text operators there
6+
7+
Clause-②: no
8+
9+
**`filter-text-operator-declared-type-refused` (protocol 18).** The entry's acceptance criteria
10+
named every text-valued field, `multiselect` / `checkboxes` / `tags` and lookup and `user` ids
11+
included, as the control that "must keep answering exactly as before". The declared-type door this
12+
entry registers still leaves every text-valued type alone. A second door, though, judges a field by
13+
how it is STORED: on a column stored as JSON it now refuses `$startsWith`, `$endsWith`,
14+
`$icontains`, `$like` and `$ilike` with `INVALID_FILTER` / `400`, as it already refused the scalar
15+
comparisons there. So a stored filter that uses one of those operators on a multi-valued field
16+
answers that `400` after the upgrade, and the old sentence called it a control.
17+
18+
The criteria now say four things:
19+
20+
- The control is a text-valued field that is NOT stored as a JSON column.
21+
- The JSON-stored population is `multiselect` / `checkboxes` / `tags`, any field declared
22+
`multiple: true` (a multi-valued lookup or `user` among them), and, on a SQL deployment still
23+
inside the ADR-0104 dual-encoding window, a single-value file-class field.
24+
- That door refuses every text operator except the membership pair `$contains` / `$notContains`,
25+
and its refusal names no declared type, so it is outside this entry's repair list.
26+
- The repair on a multi-valued field is membership: `$contains` for one member, an `$or` of
27+
`$contains` for any-of. A single-value file-class field answers text operators again once the
28+
deployment finishes the media-column move (the column step of
29+
`objectstack migrate files-to-references --apply`).
30+
31+
Text only: no entry id, `surface`, `replacement`, `reason`, conversion or refusal changes, and
32+
neither door moves. `objectstack migrate meta` prints the corrected `verify:` line, and the
33+
generated migration registry carries the same text.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ for equality alike. The check reads the field's declaration as well as its
214214
type: a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared
215215
`multiple: true` holds a list stored as JSON, so `count_distinct` over it is
216216
refused too, although the table accepts the type. The analytics service
217-
refuses the same pair with `400 DATASET_INVALID` when a query is built; this
217+
refuses the same pair with `400 DATASET_INVALID` when a query is built — or,
218+
for a field reached through a relationship path, with `400 INVALID_FIELD` when
219+
the query runs, judged on the object the path reaches; this
218220
is the identical verdict,
219221
from the identical table, one door earlier. The rule stays silent wherever the
220222
field's type cannot be resolved (an object this stack does not define, a

0 commit comments

Comments
 (0)