Skip to content

Commit 6b6bffb

Browse files
committed
Merge origin/main into claude/issue-20887-analytics-nested-relation
main's #20931 (the field-read admission gate), #20955 (the queryable-field gate), #20954 (plugin-security's comparand guard) and #20962 (relationship path objects in the admitted and scoped set) touched packages/services/service-analytics. The merge is clean at the text level; both sides' additions to analytics-service.ts and native-sql-strategy.ts are kept whole. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
2 parents 2881f47 + 5f6b63a commit 6b6bffb

208 files changed

Lines changed: 10216 additions & 842 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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The `field` liveness ledger grades `useGrouping` `live`, and the key's docblock stops describing a grouping heuristic the renderer does not use
6+
7+
Clause-②: no
8+
9+
A number field's authored `useGrouping` is honoured by the console this release builds against:
10+
an authored `true` or `false` decides whether the value renders with thousands separators, and an
11+
absent key keeps the renderer's interim rule. The `liveness/field.json` row therefore moves from
12+
`planned` to `live`, citing the objectui reader and the sites that carry the key to the number
13+
cell, and `liveness/state-counts/field.md` is regenerated to match. The `FieldSchema.useGrouping`
14+
docblock in `src/data/field.zod.ts` said the interim rule looked at a field's `min` / `max`
15+
bounds, and that the renderer half had not landed; both are corrected: the rule reads only a
16+
declared `scale: 0` (ungrouped) against any other `scale` or none (grouped), and the renderer half
17+
reads an authored value first. Text and ledger only: the schema, its `.describe()` string, every
18+
export and all runtime behaviour are unchanged.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/core': patch
3+
'@objectstack/driver-memory': patch
4+
'@objectstack/driver-mongodb': patch
5+
'@objectstack/formula': patch
6+
'@objectstack/metadata': patch
7+
'@objectstack/metadata-core': patch
8+
'@objectstack/objectql': patch
9+
'@objectstack/platform-objects': patch
10+
---
11+
12+
Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words
13+
14+
Clause-②: no
15+
16+
Many messages these packages show to authors, administrators and operators ended with an issue-tracker
17+
number where the reason belonged. The number goes, and where the sentence did not already say what was
18+
decided, it now does. Where an ADR stood beside the number, the ADR stays.
19+
20+
- Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals
21+
(the contract refuses loudly and names the mismatch, never coerces a value into storability), the
22+
kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB
23+
filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api`
24+
source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the
25+
function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate
26+
refusal says the builder used to sum an unrecognised name before this refusal existed.
27+
- The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s
28+
`engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same.
29+
- The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`,
30+
`MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card;
31+
the sentence above it already says the driver refuses rather than run or answer unisolated.
32+
- Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN
33+
translations), the `sys_email` headers help and the SCIM credential store's protection reason.
34+
- Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's
35+
excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore
36+
attestation info lines.
37+
38+
Text only: no error code, field name, status or behaviour changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
**When a `defineStack` or `composeStacks` call converts a deprecated spelling and then refuses the config, `objectstack validate --json`, `objectstack build --json` and `objectstack lint --json` now report both the refusal and the ADR-0087 conversions it applied.**
6+
7+
`defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, such as `description` on a `page:header` component (canonical `subtitle`). When the same call then refused the config, for example on an unknown `requires` token (`STACK_CAPABILITY_UNKNOWN`), each of the three commands exited 1 with the refusal's `error` and `code` and with `conversions: []`. The conversion reached stderr only, as a warn-once line.
8+
9+
The refusal now carries the conversions the producer applied before it refused (`stackConversionsOf(error)` in `@objectstack/spec`), and each command adds them to the `conversions` list of its failure payload, beside the refusal. Each conversion is listed once. A refusal whose source needed no conversion, and any other failure at load, still answers `conversions: []`.
10+
11+
Nothing is accepted or refused differently: the exit code, `error`, `code` and every other key of each payload are unchanged, and no key is added. The text face is unchanged.
12+
13+
Clause-②: no
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
Provenance comments in `@objectstack/cli`'s `bin/run.js` were re-anchored
6+
7+
Three docblock lines above `bin/run.js`'s `process.stderr` `error` listener
8+
cited a tracker number that no longer resolves on GitHub. They now cite the
9+
commit in this repository's history that made a failed stderr write non-fatal
10+
on the dev shim. The file ships because npm packs a `bin` target regardless of
11+
`files`, which is why this is a release note at all. Comment only: no command,
12+
flag, exit code, error code, export or runtime behaviour changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest): `GET /api/v1/data/:object/export` writes a `date` or `datetime` cell with a four-digit year, so an export of a year from 0001 to 0999 re-imports (#20602)
6+
7+
Clause-②: no
8+
9+
A `date` of `0500-01-01` exported as `500-01-01`, in CSV, xlsx and JSON alike,
10+
and so did the day of a `datetime` cell whose business-timezone day fell before
11+
year 1000: the instant `1000-01-01T02:00:00.000Z` exported in America/New_York
12+
as `999-12-31 21:03:58`. `POST /api/v1/data/:object/import` reads a four-digit
13+
year only, so re-importing the platform's own file refused that row as
14+
`invalid_date`. The export now spells every `date` and `datetime` cell's day
15+
with the storage rule the write doors use (`temporalStorageForm` from
16+
`@objectstack/core`): `0500-01-01` and `0999-12-31 21:03:58`, which the import
17+
reads back as the same day and the same instant.
18+
19+
**What is not affected.** Every cell whose day falls in the years 1000 to 9999
20+
exports byte for byte as before, in every business timezone and with none. The
21+
clock of a `datetime` cell is unchanged. A `datetime` stored before year 1000,
22+
which the write doors now refuse, exports with a padded year as well, and the
23+
import refuses it as `invalid_date`, as the write doors do. A year outside 0001
24+
to 9999 stays unpadded, and a `datetime` whose business-timezone day falls in
25+
such a year now spells that year as the storage rule does (`0-12-31`, not the
26+
era year `1-12-31`); the import refuses both spellings, as before.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest): an import row for a NOT NULL refusal or a unique conflict answers what the create door answers (#20701)
6+
7+
**`POST /api/v1/data/:object/import` and the async import job — a NOT NULL
8+
refusal.** When the database refuses a row because a NOT NULL column has no
9+
value (for example a field declared `storage: { notNull: true }` and not
10+
`required`, which the engine's own check lets through), the committed row
11+
now fails with `code: 'required'`, `field` set to the field, and the sentence
12+
`POST /api/v1/data/:object` gives for it ("f is required"). It used to fail with
13+
the database's own code (`SQLITE_CONSTRAINT_NOTNULL` on SQLite) and no `field`.
14+
The create door answers `400 VALIDATION_FAILED` with a `required` finding for
15+
the field; the row reports that finding the way it reports a `required` field
16+
the engine refuses itself, so no database dialect's code reaches the row.
17+
18+
**A unique conflict.** A committed row that repeats a unique value keeps
19+
`code: 'UNIQUE_VIOLATION'` and its `field`, and now carries the create door's
20+
sentence, "A record with this f already exists", in place of the engine's longer
21+
sentence (which the create door returns as `developerMessage`).
22+
23+
The async job's rows, read from `GET /api/v1/data/import/jobs/:jobId/results`,
24+
change the same way. The row takes these answers from the same mapper as the
25+
create door, as it already does for a missing database column. No key is added
26+
to the row. The dry run still previews such rows as `ok`, because it checks the
27+
metadata and does not judge `storage.notNull` or uniqueness.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest): an import row for a missing database column answers what the create door answers (#20701)
6+
7+
**`POST /api/v1/data/:object/import` and the async import job.** When an object
8+
declares a field whose database column is missing (the schema has drifted from
9+
the metadata), a committed row that writes that field now fails with
10+
`code: 'INVALID_FIELD'`, `field` set to the field, and the sentence
11+
`POST /api/v1/data/:object` gives for the same key: "The database table of
12+
object 'X' has no column for field 'f'. If the object declares 'f', its database
13+
schema has drifted from the metadata: run 'os migrate' to reconcile." It used to
14+
fail with the database's own code and text (for example `SQLITE_ERROR` and
15+
`table X has no column named f`) and no `field`. The async job's rows, read from
16+
`GET /api/v1/data/import/jobs/:jobId/results`, change the same way.
17+
18+
The row now classifies a write error through the same mapper as the create door,
19+
and takes that answer when it is `INVALID_FIELD`. The dry run still cannot see a missing column,
20+
because it checks the metadata and not the table, so it previews such a row as
21+
`ok`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): the refusal of an in-place edit or removal of a packaged flow names the clone and the on/off switch, not a redeploy or `OS_METADATA_WRITABLE` (#20819)
6+
7+
Clause-②: no
8+
9+
A write or removal that targets a flow shipped by a code package, and does not name that package, is refused with `403 NOT_OVERRIDABLE`. That covers `PUT /api/v1/meta/flow/:name` without `?package=`, `DELETE /api/v1/meta/flow/:name`, `PUT` and `DELETE /api/v1/automation/:name`, and `POST /api/v1/automation` onto a packaged flow's name. The refusal used to say "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch", and cited ADR-0005. The administrator of an installed package can do neither.
10+
11+
The refusal now names the two paths ADR-0126 sanctions for a packaged flow, and cites ADR-0126:
12+
13+
- clone it under a new name to customize it: `POST /api/v1/automation/:name/clone` with `{ name, label }`;
14+
- or switch it off: `POST /api/v1/automation/:name/toggle` with `{ enabled: false }`. Where one install serves several organizations, only the platform operator can use the switch.
15+
16+
The status, the code and which writes are refused are unchanged. `OS_METADATA_WRITABLE=flow` still opens the lock as before; the refusal just no longer suggests it. Every other metadata type's refusal reads exactly as before. A write that names the shipping package with `?package=` is refused with `403 ITEM_LOCKED` by a separate limb, which this change leaves as it was.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/metadata': patch
3+
'@objectstack/objectql': patch
4+
'@objectstack/driver-memory': patch
5+
---
6+
7+
fix: the whole-day bound on a bare `YYYY-MM-DD` upper bound is applied at the seams only — `DatabaseLoader.queryHistory` in driver mode becomes one, the engine seam lowers type-blind for an object with no field map, and `InMemoryDriver` drops its own copy (ADR-0053 D-D1 items 5 and 7, #20822)
8+
9+
Clause-②: no
10+
11+
- **`@objectstack/metadata` — `DatabaseLoader.queryHistory` in driver mode lowers its own filter.** With a raw `IDataDriver` (`MetadataManager.setDatabaseDriver`) the history filter reaches the driver without passing any seam. The loader now runs the shared `lowerFilterCondition` (`@objectstack/spec/data`) on it, typed by the history object it syncs: `until: 'YYYY-MM-DD'` reads `recorded_at < next day`, so every version recorded on that day is kept on every driver, and an instant `until` is kept as written. Engine mode is unchanged (the engine's own `where` seam lowers it). Before this, the whole day was kept only by each driver's own copy of the rule; with `@objectstack/driver-memory`'s copy deleted below, `until` = today would have gone from every version of the day to none.
12+
- **`@objectstack/objectql` — an object with no field map is lowered type-blind.** The engine's `where` seam (on `find`, `findOne`, `count`, `update`, `delete` and `aggregate`'s `where` / `aggregations[i].filter`) reads the object's declared field map and rewrites a declared `datetime` column only. For an object the registry does not hold there is no declaration to read, and the seam now applies the whole-day rules to every column (a bare-day `$lte` becomes `$lt` the next day, a `$between` splits), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the declared type. It used to leave such an object to each driver's own copy. Visible on `SqlDriver`: a bare-day `$lte` on a non-`datetime` column of an unregistered object that holds ISO instant text now keeps the whole day; a `datetime` or `date` column answers as before. An object with a field map is unchanged.
13+
- **`@objectstack/driver-memory` — `InMemoryDriver` compiles the comparison it is handed.** Its four copies of the whole-day rule are deleted (the `$lte` and `$between` arms of the filter translator, the `<=` and `between` arms of the AST-node translator). A read through the engine hands it a `where` the engine's seam has already lowered, so on that path a declared `datetime` column keeps the whole named day, and a declared `date` column answers as before. A row-level security `using` filter is not lowered by the engine's seam: the security middleware ANDs it into the query's `where` after that seam has run, and only the RLS compile seam lowers it, rewriting just the columns its field guard declares `datetime`. Two answers converge on what `SqlDriver` already returns (ADR-0053 D-D1 item 7's scope): on a registered object, a bare-day `$lte` / `$between` on a declared `text` column holding ISO instant text, or on a column the object does not declare, is now compared as written, where this driver used to widen it to the whole day. One path narrows outside those two: an RLS `using` policy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, is compiled with no field guard, so the RLS compile seam reads no column as `datetime` and the bound reaches this driver as written, where this driver used to widen it to the whole day; that holds until #20822 group 2 makes the RLS compile seam type-blind when it has no guard. A direct `find()` that passed no seam gets the comparison it wrote (item 5); lower the filter with `lowerFilterCondition` first to keep the whole-day reading.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/spec': patch
4+
'@objectstack/objectql': patch
5+
'@objectstack/rest': patch
6+
---
7+
8+
fix(objectql,rest): a `date` or `datetime` value refused for its year says so — "must be a date in the years 0001 to 9999" / "must be a datetime whose UTC year falls in the years 1000 to 9999" — instead of "must be a valid date (ISO-8601)", which was false for a value such as `0500-07-15T10:00:00Z` (#20846)
9+
10+
Clause-②: yes (widening) — one new export on `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`. No value's verdict moves and no wire key moves: the field code stays `invalid_date` and its `constraint` stays `{ type }`.
11+
12+
`POST` / `PATCH /api/v1/data/:object` and each row of `POST /api/v1/data/:object/import`
13+
refuse a `date` outside the years 0001 to 9999 and a `datetime` whose UTC year falls
14+
outside 1000 to 9999. When the value itself is readable — an ISO 8601 string such as
15+
`0500-07-15T10:00:00Z` or `+010000-01-01`, or a `Date` — the refusal's message now
16+
names the kind's years. An author who read "not valid ISO" rewrote the spelling, and no
17+
spelling of that year is admitted.
18+
19+
- `@objectstack/spec`: the validation message catalog gains `invalid_date_range` and
20+
`invalid_datetime_range` in `en`, `zh-CN`, `ja-JP` and `es-ES`. They are two more
21+
sentences of the `invalid_date` code, never a wire value. The years are the template
22+
parameters `{{firstYear}}` / `{{lastYear}}`. A deployment that overrides a message
23+
under `validation.field.invalid_date` or `validation.field.invalid_datetime` does not
24+
cover these values. To override their text, define
25+
`validation.field.invalid_date_range` / `validation.field.invalid_datetime_range`.
26+
- `@objectstack/core`: `SUPPORTED_TEMPORAL_YEARS` (`{ date: { first: 1, last: 9999 },
27+
datetime: { first: 1000, last: 9999 } }`, frozen) is the range
28+
`isOutsideTemporalYearRange` judges by. It is exported so a refusal names the range
29+
from the source the doors use, never a copy of its numbers.
30+
- `@objectstack/objectql` and `@objectstack/rest`: the record validator and the import's
31+
cell reader choose the range sentence for such a value. An import cell with more than
32+
four year digits (`+010000-01-01`) is refused by the import's reader. It used to read
33+
"is not a valid date" and now gets the same range sentence as the write door.
34+
35+
**What is not affected.** Which values are refused is unchanged, and so is the refusal's
36+
code (`invalid_date`) and `constraint`. A value that is not readable keeps its sentence:
37+
"must be a valid date (ISO-8601)" at the write door, `"…" is not a valid date` at the import.
38+
So does a number, which is never a written `date` or `datetime`.

0 commit comments

Comments
 (0)