Skip to content

Commit 7ca00ef

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14972-injected-system-column-labels-localised
2 parents 570a663 + 8e500f2 commit 7ca00ef

92 files changed

Lines changed: 7164 additions & 795 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
The `no_sign_in_account_at_boot` report now names a remedy that works — and warns off the one that silences the report itself.
6+
7+
That boot line fires on the deployment nobody can sign in to: human `sys_user` rows, zero `sys_account` rows. It ended with two remedies, and measured on the exact population it fires on, neither did what its sentence said:
8+
9+
- **"Open the audience posture so an existing person can register their own login"** produced no login, and for an existing person it never can: self-registration is a user-creation path, so it cannot attach a login to an address that already carries a `sys_user` row, whatever the posture. Widening only ever admits a *new* address — and then every posture other than `invite_only` forces `requireEmailVerification` on, so that login is refused `EMAIL_NOT_VERIFIED` at its first sign-in, and a locked-out self-hosted install is usually the shape with no mail transport wired.
10+
- **"Write a `sys_account` credential row directly against the store"** was worse than useless. The `password` column carries a secret in the platform's own hash format, so a plaintext one authenticates nothing — and the probe behind this report asks only whether *any* `sys_account` row exists, so writing one turns the report off. The operator's first attempt at the named remedy turned the loud dead end back into the silent one the report was written to end.
11+
12+
The line now names the path that was measured to work: write one pending `sys_invitation` row directly against the store — a lowercase address the directory does not already hold, `status` `pending`, a future `expires_at`, `inviter_id` of any existing `sys_user` — then register through the ordinary sign-up endpoint. The invitation carve-out admits that one creation under every posture, so no door needs widening. It is an admission verdict and not a verification bypass, though, so the line scopes what follows from that: only under the default `invite_only` posture is the recovery mail-transport-free, and it tells the operator to close a widened posture back to `invite_only` before the invited person registers — otherwise the invited login is created, refused `EMAIL_NOT_VERIFIED` at first sign-in, and has silenced this report on the way past. On the `single` tenancy posture that account holder is then promoted to platform admin. The other two are still named, as the two things that look like remedies and are not, because an operator who is going to hand-write a credential row anyway needs to know it blinds the probe.
13+
14+
**Message text only — no admission semantics move.** Nothing widens, nothing narrows, no accept set changes, and the probe is untouched: this changes what an operator *reads*, not what the platform *admits*. The long form of the same three facts is on the self-hosting deployment page.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`chart-measure-unknown` no longer blocks a build over a chart `series[].name` (or a page chart's `yAxis[].field`) that names nothing — those positions are presentation, and the message now says so.
6+
7+
The rule fired at `error` on every measure position of the three chart surfaces it covers, with one consequence sentence: *"result rows are keyed by MEASURE NAME … so this series comes back empty"*. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), that is true only where the position feeds the dataset query, and the three surfaces do not agree:
8+
9+
- **Report charts** run the chart's own query out of the two axis strings (`useDatasetRows(dataset, [xAxis], [yAxis], …)`*"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*), so `chart.xAxis`/`chart.yAxis` are the binding. `chart.series[]` is *"the author's per-chart override for ONE measure's display name"*, lowered through `mergeAuthoredSeries`, where *"an authored entry naming a measure that is NOT in the dataset selection is **ignored** — membership belongs to the dataset"*.
10+
- **List-view charts** have no presentation position at all: `ListChartConfigSchema` is a strict object of `chartType`/`dataset`/`dimensions`/`values`, and `values[]` is handed to the chart as the dataset measures.
11+
- **Dataset-bound page chart components** query `{ dimensions, measures: values }` and then replace the authored series wholesale with one derived entry per selected measure, so `properties.series[].name` reaches the renderer not at all and `properties.yAxis[].field` re-points nothing.
12+
13+
**Behaviour change users see:** the three presentation positions — report `chart.series[].name`, page-component `properties.series[].name` and `properties.yAxis[].field` — drop from `error` to `warning`. A build or a metadata publish that used to be refused because of one of them now succeeds, with the finding on the advisory channel. The finding is KEPT, not deleted: the metadata really is wrong — the author wrote a key and believes it is in force. Every query position (report `chart.yAxis`, and `values[]` on all three surfaces) keeps `error` and its existing message verbatim.
14+
15+
Two smaller corrections ride along, both from the same read:
16+
17+
- The page surface's `yAxis[].field` refs are no longer concatenated into the `series[]` limb before the measure walk, so an axis position no longer takes the series message. Reading both shapes on that surface stays deliberate; giving them one sentence was not.
18+
- `chart-axis-not-selected` (a declared measure outside the selection) took the same one-size consequence, *"the query does not return it, so the series plots nothing"*. It keeps that wording at a query position and states the real one at a presentation position, where no series is derived for the name in the first place.
19+
20+
Note that none of these three surfaces declares `suppressWarnings` — it is a dashboard-widget key — so the new advisories cannot be individually silenced; the hint says so instead of pointing at a key that does not exist.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os validate|info|diff|lint|compile|build|verify|migrate meta|i18n check|i18n extract --json` no longer print human text on stdout when the config file is missing.
6+
7+
`resolveConfigPath()` emitted both of its refusals — the explicit-path miss and the auto-detect miss — through `printError` and `console.log`, **both of which write to stdout**, and then called `process.exit(1)` directly. Ten published `--json` faces reach that helper, so a missing config file answered them with exit 1, an unparseable stdout and an **empty stderr**: 206 bytes of prose on the one stream `--json` reserves for the machine. And because the exit was called rather than thrown, every command's catch-all `--json` error exit — all of which sit downstream of a throw — never ran.
8+
9+
The diagnostic now goes to stderr, where the rest of this CLI's diagnostics already go. Nothing else moves:
10+
11+
- **the exit code is still 1**, so a consumer branching on exit status sees no change at all;
12+
- **the wording is unchanged**, hints included, so a human reading a terminal sees the same three lines;
13+
- **nothing is accepted or rejected differently** — no config that loaded before fails now, and none that failed now loads.
14+
15+
⚠️ **No error payload is invented on this path.** What a `--json` consumer should *receive* when the config file is missing is an envelope question that touches ten published faces at once, and it is deliberately left open here — this change settles only that the machine's channel no longer carries prose. `--json` on this path emits nothing on stdout; a consumer must still read the exit status, exactly as it must today.
16+
17+
A new pin (`config-miss-stdout-purity.e2e.test.ts`) drives all ten faces on both branches of the helper. The existing purity pin could not: it discovers its family as the commands that call `bootSchemaStack`, and these fail before any kernel boots.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
An organization-scoped caller's own items now appear in the untyped metadata diagnostics sweep.
6+
7+
`GET /api/v1/meta/diagnostics` has two arms. The `?type=` arm has stated the caller's organization since #13753; the untyped whole-registry sweep passed none, so the Studio governance summary reported clean tiles over a partition it never read — undercounting relative to the per-type drill-down screen you reach by clicking into it. A summary whose whole job is surfacing problems, and which structurally cannot see a class of them while its own drill-down can, issues a false all-clear. The untyped arm now forwards the caller's organization, so items that organization authored on the five `allowOrgOverride: true` types (`view`, `dashboard`, `report`, `translation`, `email_template`) are counted in `stats`, `total` and `scannedItems`.
8+
9+
The organization is passed RAW, deliberately, and that is the whole of the change — no new parameter, response field, status code or contract surface. There is no single type to fold on for a whole-registry sweep, and folding on any one of them would suppress the organization for every type at once; instead `getMetaDiagnostics` reads each swept type through `getMetaItems`, which applies the `allowOrgOverride` read gate to its own request type, so every type is scoped on its own registry flag. A non-overridable type (`object`, `flow`, `app`, …) is still read environment-wide and no pre-#6190 organization-scoped row is resurrected into the report. An anonymous or organization-less caller reads exactly what it read before, and the `stats` / `total` / `scannedTypes` arithmetic is unchanged in shape.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/driver-memory": patch
3+
---
4+
5+
fix(driver-memory): the reference matcher's `$notContains` arm answers the predicate, not a type test, for a stored non-string value
6+
7+
`match()` used to answer `{ n: { $notContains: '5' } }` with NO for `{ n: 5 }` — the arm read `typeof value !== 'string' || value.includes(target)`, so a number failed `$contains` (correct) AND its negation (wrong: for the very reason a number cannot contain the substring, it does not contain it). This package's own live mingo path admitted the row, so one filter answered two ways depending on which face was asked; on this face the failure mode was silently dropped rows.
8+
9+
The arm now answers what `FILTER_TEXT_CASES`' new `score` rows declare on every face (maintainer ruling 2026-09-05 on the contract card): a stored value that is not a string never satisfies a positive text operator and always satisfies `$notContains`. The no-value cells keep their #13166 answer; nothing else in the matcher moved.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/driver-sql": minor
3+
---
4+
5+
A text operator over a column whose declared type stores no text (`Field.number` and its numeric siblings, `Field.boolean`) now compiles to the contract's declared answer on every dialect, instead of a dialect accident.
6+
7+
Before: `{ score: { $contains: '5' } }` over a numeric column compiled `col GLOB '*5*'` on SQLite and coerced the REAL in its storage class's spelling (`5` as `'5.0'`, so `$endsWith: '0'` matched every row), `col LIKE $1 ESCAPE $2` on Postgres and was refused at query time with SQLSTATE 42883 (`operator does not exist: real ~~ text` — a 500 for a filter the spec accepts), and `CAST(col AS BINARY) LIKE ?` on MySQL.
8+
9+
Now (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05): the positive operators (`$contains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) compile to `1 = 0` and `$notContains` to `1 = 1` — the same row set as every JS face, decided from the declared type at compile time because the stored value is not visible until run time. Postgres: a 500 becomes a result. The gate reads the `numericFields` / `booleanFields` registries `initObjects` and `registerExternalObject` already fill; a table this driver was never told about keeps the `LIKE` / `GLOB` it always compiled, every comparand refusal still runs first, and the constants compose with the NULL-safe rules (`$notContains` admits a NULL row already) and the `$not` rewrite. Temporal columns are untouched: their stored value IS text on SQLite, so the contract declares nothing for them.
10+
11+
`driver-sqlite-wasm` and `driver-turso`'s local transport inherit this compiler.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/driver-turso": minor
3+
---
4+
5+
The remote transport compiles a text operator over a declared numeric or boolean column to the contract's declared answer, in step with the local transport.
6+
7+
`RemoteTransport.buildWhereSQL` compiles filters independently of `SqlDriver` and keeps no schema, so a text operator over a `Field.number` used to compile `"col" GLOB ?` and coerce the REAL in the storage class's spelling (`5` as `'5.0'`). `TursoDriver` now hands the transport its declared-type rule (`setNonTextColumnResolver`, the same shape as the temporal `setFilterColumnSql` rule), answered from the registries `registerRemoteFieldMetadata` already fills at schema sync — so a positive text operator over such a column compiles to `1 = 0` and `$notContains` to `1 = 1` on BOTH transports (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05), instead of a dialect accident. A transport nobody handed the rule to compiles exactly as before, and every comparand refusal still runs ahead of the constant.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`FILTER_TEXT_CASES` declares what a text operator answers over a stored value that is NOT a string, and the fixture gains its first non-string column.
6+
7+
Measured before this row existed, one filter over one numeric column answered four ways across the platform: `driver-memory`'s reference matcher said NO to `$contains` and to `$notContains` for the same row; its live mingo path, `formula`, objectql's `having`, `driver-mongodb` and the analytics face type-gated (`$contains` NO, `$notContains` YES); the SQLite family coerced the number to text in its storage class's spelling (REAL renders `5` as `'5.0'`); and live Postgres refused at query time with SQLSTATE 42883 — a 500.
8+
9+
The maintainer ruled the cell on 2026-09-05 (option A, type-gate): a stored value that is not a string never satisfies a positive text operator (`$contains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) and satisfies `$notContains` — complementarity holds, on every face. Coercion was refused on the measurement; a declared-type door that refuses the filter before any backend runs is deferred to its own decision card, not rejected.
10+
11+
- `FilterTextRow` is now `{ id, name, score }``score` is a NUMBER on every row (a `0` among them), chosen so a coercing backend answers a visibly non-empty set and a truthiness guard drops a row.
12+
- Five new evaluated rows over `score`: the four positive operators the table can carry answer `[]`, `$notContains` answers all nine. (`$like` / `$ilike` follow the same rule and are pinned on the faces that answer them — the table is a driver's enrolment and `driver-mongodb` refuses those two.)
13+
- `NON_TEXT_STORED_VALUE_TYPES` (`field-value.zod.ts`) — the numeric and boolean value classes, i.e. the declared field types whose stored value is never text — is the list the SQL faces classify a column by at compile time, since they cannot read the value. Temporal types are deliberately absent: their stored form is a dialect question (ADR-0053) the row does not decide.
14+
15+
Every suite that materialises the fixture adds the column (SQL `initObjects` DDL included).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
---
4+
5+
The three SQL compilers in this package — the RLS read-scope lowering (`compileScopedFilterToSql`), `NativeSQLStrategy`'s own `where` and the `ObjectQLStrategy` SQL echo — compile a text operator over a column whose declared type stores no text to the contract's declared answer.
6+
7+
`compileScopedFilterToSql(filter, alias, options?)` takes a new optional `nonTextColumn(field)` predicate; when it answers `true`, a positive text operator compiles to `1 = 0` and `$notContains` to `1 = 1` instead of a `LIKE` that coerces on SQLite (`5` renders `'5.0'`) and is refused at query time on Postgres (SQLSTATE 42883 — a 500 on a read scope the platform accepted). The service answers the predicate from the field metadata hook it already holds (`sourceFieldMeta`), exposed to strategies as `DatasetScopedStrategyContext.declaredFieldType`, and the two strategies pass it for the read scope and for the query's own text filters, so a query and its RLS scope answer one cell one way and the echo prints the statement that ran (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05). A host that wires no field metadata keeps the `LIKE` it always got, and every comparand refusal still runs ahead of the constant.
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+
feat(spec): `SharingRuleEvaluationResult` declares `grantsRefused?: number` — the optional seventh key the sharing-rule evaluate route already answers (#14969)
6+
7+
`minor`, derived: a new key on a published contract interface is additive public
8+
API (semver "backwards-compatible functionality"), and not `major` because the
9+
key is **optional** — every existing `ISharingRuleService` implementer, in-tree
10+
and out, keeps compiling unchanged, and every consumer typed against the six
11+
counts keeps reading them.
12+
13+
`POST /api/v1/sharing/rules/:idOrName/evaluate` (ledgered `sdk`,
14+
`shares.rules.evaluate`) passes the service's return value through unfiltered,
15+
and `@objectstack/plugin-sharing` has counted refused grants on its own subtype
16+
since #14754 — so the wire carried `grantsRefused` while the declared client
17+
type (`client.shares.rules.evaluate`, typed `Promise<SharingRuleEvaluationResult>`)
18+
could not name it without a cast. The client gains the key through its spec
19+
import with no edit of its own.
20+
21+
What the key means, and what its absence means: it counts the grants the
22+
engine **refused** during the pass (`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` on
23+
an organization-less insert into a tenant-scoped `sys_record_share`); the pass
24+
continues past a refusal, so `grantsRefused > 0` is not a failed pass. The key
25+
is **absent — not `0`** — from any implementation that does not count
26+
refusals. A consumer branching on it must read "unset" as "this implementation
27+
does not report refusals", never as "no grant was refused"; only a present `0`
28+
says the latter. Do not `?? 0` it.
29+
30+
Optional in the spec composes with the plugin-local narrowing: an
31+
implementation that counts refusals may require the key on its own subtype
32+
(`SharingRuleReconcilePassResult extends SharingRuleEvaluationResult`), a legal
33+
covariant narrowing that still satisfies `ISharingRuleService`.

0 commit comments

Comments
 (0)