Skip to content

Commit ce6d3e9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20439-hook-condition-expression-row
2 parents 18da137 + acd0095 commit ce6d3e9

60 files changed

Lines changed: 2863 additions & 140 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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): `ApiError.code` and a flattened list overlay's legacy `options` bag carry the shapes their doors accept (#19920)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for TypeScript code that annotates with `ApiError`, with any response type built on `BaseResponseSchema` (`BaseResponse`, `BatchUpdateResponse`, `SessionResponse`, the metadata, package, storage, analytics and automation response types, and the rest), with `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` or `AssembledViewArtifactParsed`, or with the input type of a schema returned by `makeApiErrorSchema`**: a narrowing of published TYPES, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). The runtime accept set does not move at all: no schema's parse, no value and no export changes, and no export is added.
10+
11+
Two places in the published types were wider than the doors that judge the same bodies, so values those doors refuse type-checked:
12+
13+
- `ApiError.code` (the INPUT type of `ApiErrorSchema`): FROM `unknown` TO `ErrorCode`, the vocabulary the schema parses against (`StandardErrorCode` and the registered ledger codes). `ErrorCode` was cast to `z.ZodType` with its output type only, and `z.ZodType`'s input type defaults to `unknown`, so `{ code: 42, message: 'x' }` compiled as an `ApiError` while the schema refuses it at `code`. The same `code` narrows in the `error` of every response envelope built on `BaseResponseSchema`, and in each `ApiError` row of a batch result. `makeApiErrorSchema(codes)` had the same cast for a caller-supplied vocabulary: its schema's input `code` is now the standard catalogue plus `codes`, where it was `unknown`. The parsed types (`ApiErrorParsed`, the `…Parsed` response types) do not move: their `code` was already typed.
14+
- A flattened list overlay's legacy `options` bag: FROM a string-keyed record of `unknown` TO one optional entry per list kind that has a block (`calendar`, `chart`, `gallery`, `gantt`, `kanban`, `map`, `timeline`, `tree`), each entry that kind's own block with every key optional. This holds on the list overlay member of `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`. `options: { foo: 1, kanban: 42 }` type-checked as all four while that member refuses both keys.
15+
16+
**If your code stops compiling.** A value you annotated with one of these names is not the shape the door accepts: correct it, or type a value that is still unvalidated as `unknown` and let the schema's `safeParse` decide. An error `code` is a member of `ErrorCode` (or, for a `makeApiErrorSchema` schema, of the standard catalogue plus the codes you supplied); a producer whose own code is outside the vocabulary reports it on `declaredCode`, not `code`. An `options` bag carries only the per-kind blocks listed above, each judged key by key like the top-level block of the same kind; `grid` has no block, and its settings are top-level keys of the view.
17+
18+
The declared types of `ErrorCode` and of `makeApiErrorSchema`'s `code` narrow with them, so `z.input` of each is typed where it was `unknown`. The types are the schemas' declared shapes, not their verdicts: refinements are not types, so each schema remains the only judge.
19+
20+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves — no spec key, no export and no stored row changes, and every runtime accept set is unchanged, so `objectstack migrate meta` has nothing to reach — and only TypeScript annotations narrow, whose channel is the consumer's compiler. -->
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/spec": minor
4+
---
5+
6+
feat(objectql,spec)!: a numeric field's declared `precision` ("Total digits") is enforced on writes — a value that needs more digits is refused with field code `max_precision` (#19992)
7+
8+
Clause-②: yes
9+
10+
**BREAKING** — a narrowing of the write accept set on `@objectstack/objectql`, shipped as `minor` under the repo's launch-window convention (`check-changeset-no-major` refuses `major` until GA); the breaking-ness is carried by this banner and the ADR-0087 disposition, never by the level. Nothing an author writes changes spelling: `precision` keeps its key, its type and its legality.
11+
12+
`FieldSchema.precision` was declared ("Total digits") and read by nothing. Every numeric column is the fixed exact decimal of `NUMERIC_COLUMN_REPRESENTATION`, the record validator had no branch for it, and the renderer reads the liveness ledger cited are gone, so `precision: 5` on a `number` stored `123456789` verbatim. The metadata designer writes the key (labelled Precision, beside Scale), so it was a setting an author could make and see nothing come of. It is now enforced at the one place a write is judged.
13+
14+
**`@objectstack/objectql`** — the record validator refuses, after `min` / `max` and `max_scale`, a `number`, `currency`, `percent`, `rating` or `slider` value whose digit count exceeds a declared `precision`. It refuses with `400 VALIDATION_FAILED` and the field code `max_precision`, and it never rounds. The count is the SQL `DECIMAL(p, s)` one, taken on the stored value:
15+
16+
- **With a `scale`**, digits are counted at the field's decimal places, so the integer part may carry `precision − scale` digits. `precision: 5, scale: 2` holds up to `999.99` and refuses `1234.5`, which is `1234.50`, six digits.
17+
- **With no `scale`**, the value's own digits count. Leading zeros never count, and trailing zeros of the integer part always do: under `precision: 4`, `0.001` fits and `10000` does not.
18+
- **On `currency`**, where `scale` is refused, an amount counts at its own decimals. The decimals themselves stay unconstrained, and only the total is bounded: `precision: 18` refuses a 19-digit amount.
19+
- **On a fraction-stored `percent`** the count is taken two places further right (`scale + 2`, or 2 with no `scale`). The count is then the percentage-point value's digits as displayed: `precision: 4, scale: 2` holds 99.99% and refuses 100%.
20+
21+
What an author with an oversize value sees: the write is refused, nothing is stored, and the field error names the declaration and the count. For example, `constraint: { precision: 5, scale: 2, actual: 6 }` renders as "Hourly rate must have at most 5 digits in total, counting 2 decimal places (got 6)" in four locales. The REST create, batch, update and import routes all answer it, and `validate` (the dry run) predicts it. Only NEW writes are judged: a stored value longer than a `precision` declared later is never re-read. Nothing changes in storage or DDL.
22+
23+
The fix is one of three. Write a value that fits. Raise `precision` to the digits the field really holds. Or delete the key if the number was meant as decimal places: those are `scale`, and a currency's decimal places are its ISO 4217 minor unit.
24+
25+
**`@objectstack/spec`** — `FieldErrorCode` (the ADR-0114 field-level catalog) gains `max_precision` beside `max_scale`. `BUILTIN_VALIDATION_MESSAGES` gains its two sentences, `max_precision` and `max_precision_scaled`, in `en` / `zh-CN` / `ja-JP` / `es-ES`. `FieldSchema.precision`'s describe now states the counting rule and where it is enforced. The `precision` row of the field liveness ledger is re-evidenced at the write seam.
26+
27+
**Who is affected, measured** on `origin/main` `df3ba164`: no example app, template, platform object, seed or JSON fixture in the tree declares a field-level `precision`. Two test fixtures do (`precision: 5, scale: 0` on a 1–12 hours field), and every value they write fits.
28+
29+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: `precision` keeps its key, its type (`z.number().int().min(0)`) and its legality on every field type, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What narrows is the record validator's write accept set for values under an already-declared count, which is runtime behaviour, not an authored shape. The spec edits add a member to a closed enum, two message templates and a describe; none removes or renames anything an author can write. The other categories are closed on facts: both packages publish (not `unpublished`); no ADR-0087 id is minted here and none covers this (not `registered` / `already-registered`); and runtime behaviour changes, not only a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/driver-turso': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(driver-turso)!: `new TursoDriver` refuses `syncUrl` under a forced `mode: 'remote'`, and `sync` with no `syncUrl`, instead of building and ignoring them
7+
8+
Clause-②: no (narrowing) — nothing is widened. No key is added, removed or renamed, and no exported symbol moves. Two driver configurations that the turso driver used to build and then ignore a key of are now refused when it is built.
9+
10+
`new TursoDriver()` accepted `syncUrl` beside a forced `mode: 'remote'`. Remote mode sends every read and write straight to `url`, and the remote client is created without `syncUrl`, so no replica is built and no sync ever runs. Measured on the built driver before this change, a `libsql://` or `file:` url under `mode: 'remote'` with `syncUrl` and `sync` constructed and connected, and `isSyncEnabled()` answered `true`. No sync interval started, and the sync call rejected with `SYNC_NOT_SUPPORTED` (`SyncNotSupported("File")` on the `file:` url). It also accepted `sync` with no `syncUrl`, in any mode, where nothing reads it. `@objectstack/spec`'s `TursoConfigSchema` already refused both at authoring. A datasource row stored before that, or a config a host builds itself, reached the constructor unparsed and ran with a sync setting that did nothing.
11+
12+
**BREAKING** accept-set narrowing on a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now with `VALIDATION_ERROR` / 400, before any client or database is opened:
13+
14+
- `syncUrl` under a forced `mode: 'remote'`. A remote url beside `syncUrl` with no `mode` was already refused, as a replica on a remote url;
15+
- `sync` with no `syncUrl` (or with an empty one), in local, replica and remote mode alike.
16+
17+
Each refusal's message is the spec contract's issue message for that key, byte for byte, so authoring and boot say the same thing. A test holds the copies equal. The spec's `syncUrl` message said the driver "runs no sync, so the setting changes nothing". It now reads "the turso driver refuses this configuration when it starts", like its sibling refusals, and the driver's own `TursoConfigSchema` mirror follows (`@objectstack/spec` patch: message text only). The ADR-0087 entry `turso-config-transport-mismatch-refused` now also records that the constructor refuses these two shapes at boot.
18+
19+
Left accepted on purpose: `mode: 'replica'` on a `file:` url with no `syncUrl` (and no `sync`). It still runs as a plain local database. Refusing it in the constructor alone would refuse a config both schemas accept, so it is tracked separately.
20+
21+
### Migration: FROM → TO
22+
23+
| You wrote | Write instead |
24+
| --- | --- |
25+
| `url: 'libsql://my-db.turso.io', mode: 'remote', syncUrl: …` (with or without `sync`) | a remote database: drop `syncUrl` and `sync`. An embedded replica: `url: 'file:./data/replica.db', syncUrl: 'libsql://my-db.turso.io'` and no `mode` |
26+
| `url: 'file:./data/app.db', mode: 'remote', syncUrl: …` | the same two ways out |
27+
| `sync: { … }` with no `syncUrl` | name the remote in `syncUrl` (with a `file:` url), or drop `sync` |
28+
29+
A datasource row stored with one of these shapes is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted, reported sync as enabled and never synced. The way out is the table above: drop `syncUrl` / `sync` from a remote config, or use a `file:` url with the remote in `syncUrl`.
30+
31+
Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors either shape, and no in-repo caller reads `isSyncEnabled()` or calls the driver's sync outside `@objectstack/driver-turso`'s own tests. Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero.
32+
33+
<!-- adr-0087: not-required (already-registered turso-config-transport-mismatch-refused) that entry is this family's semantic TODO (the turso transport refusals, registered with the authoring half), and this diff updates its stored-row sentence to cover the constructor half: syncUrl under a forced remote mode and sync with no syncUrl are refused at boot as well -->

‎.changeset/20331-validate-view-container-name.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ derived object key is empty, because the boot registrar skips that entry with a
2929
warning and never refuses it. The boot registrar now calls this function. What it
3030
refuses, its message and its `VALIDATION_ERROR` / `400` envelope are unchanged.
3131

32-
Not changed: `os build` does not run this check, so it still writes an artifact
33-
carrying such a container, and the server refuses that artifact when it loads it.
32+
`os build` runs the same check as well (#20393, its own entry), so it no longer
33+
writes an artifact carrying such a container.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/plugin-dev": patch
3+
---
4+
5+
`DevPlugin` now loads `@objectstack/setup` and `@objectstack/account` through literal `import('…')` specifiers, like every other declared dependency it loads, instead of one variable specifier shared by a loop (#20376).
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** The setup / account app-package loop imported `spec[0]`. A variable specifier cannot be resolved when the file is transformed. Under vitest, every `DevPlugin.init()` in a test therefore made two round trips to the main test process, even with both packages mocked, inside every clocked test window that boots `DevPlugin`. The main process is shared by the whole run, so on a busy CI shard those round trips wait on other files' work, against this package's 5000 ms test budget.
10+
- **What changes.** Each loop entry carries its own literal loader. The `try` / `catch` and the absent-package report around each load are unchanged: a missing package is still logged as, for example, `✘ @objectstack/setup not installed — skipping its app`, and a present one that fails is still reported as present-but-failed.
11+
- **Unchanged.** Nothing an author or operator configures or sees changes. Both packages were already declared dependencies of `@objectstack/plugin-dev`, and both the ESM and the CJS build keep a native `import("…")` for each.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os build` / `os compile` refuses a `views:` container whose own `name` disagrees with the object it binds to, and writes no artifact the server would refuse at boot (#20393)
6+
7+
Clause-②: no
8+
9+
A view container is registered under the object it binds to. When its own `name`
10+
is set to something else, for example `{ name: 'order_line', object: 'my_app_order_line', list: { … } }`,
11+
the server refuses the whole stack at boot. `os validate` has refused that stack
12+
since #20331, but `os build` still exited `0` and wrote `dist/objectstack.json`
13+
carrying the container, so `os serve` then refused the artifact it was handed.
14+
15+
`os build` now runs the same check `os validate` runs, right after the schema
16+
check and before anything is written, and prints the message the server prints
17+
at boot. The text form and `--json` both exit `1`, and no artifact is written. The
18+
`--json` failure payload is `{ success: false, errors, warnings, conversions }`,
19+
with one `errors` entry per refused container: `path` (for example `views[0]`, or
20+
`packages[1].manifest.views[0]` in a multi-package stack), `code: 'VALIDATION_ERROR'`,
21+
`httpStatus: 400` and `message`, the same rows `os validate --json` reports. A stack
22+
the server accepts builds exactly as before, with the same output.
23+
24+
**Fix:** remove the container's `name`, or set it to the object name the message names.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
`component-props-invalid` no longer hides a wrong `object` prop on a component that also carries a `dataSource` binding
6+
7+
Clause-②: no
8+
9+
A page component whose `dataSource.object` names the object may leave the flat
10+
`properties.object` shorthand out: the binding supplies it, so the rule does not
11+
report the props schema's required `object` as missing. That waiver was matched
12+
on the issue's path alone, so it also swallowed every other issue the props
13+
schema raised at `object`. A present but wrong value, such as `object: 7` or
14+
`object: null`, was reported without a binding and silently passed with one.
15+
16+
The waiver now covers what its contract says: a missing `object`, meaning no
17+
key or an explicit `undefined`. A value the author did write is judged as
18+
written, and it is reported at `properties.object` exactly as it is on the same
19+
component without a binding.
20+
21+
Effect on `os validate`, `os lint` and `os build`: a document that sets both a
22+
`dataSource` binding and a wrong-typed `properties.object` now gets one
23+
`component-props-invalid` warning it did not get before. The rule stays
24+
advisory: without `--strict` nothing that validated before is refused; under
25+
`os validate --strict` or `os lint --strict` the new warning fails the run, as
26+
every warning does. A component that binds
27+
through `dataSource` and omits `properties.object` is still clean.

0 commit comments

Comments
 (0)