Skip to content

Commit ebd8af3

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

124 files changed

Lines changed: 10135 additions & 1122 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
Clause-②: no
7+
8+
Two live structured object keys are authorable in the metadata form: `fieldGroups` and `indexes`. Each was **declared** by `ObjectSchema`, graded `live` by the liveness ledger, and offered by **no** form in `METADATA_FORM_REGISTRY`, so an author's only door was the Source tab. Each is now a `type: 'repeater'` row on the object form whose sub-rows are declared by hand rather than derived from the schema:
9+
10+
- `fieldGroups` (Basics, beside `highlightFields`) — six sub-rows, one per canonical group key: `key` and `label` (required text), `icon` (text), `description` (textarea), `collapse` (a `none` / `expanded` / `collapsed` select) and `visibleWhen` (`type: 'code'`, `language: 'expression'`, the `fields` grid's predicate rows). The three `[DEPRECATED → collapse]` aliases (`defaultExpanded`, `collapsible`, `collapsed`) are **not** offered; the metadata-form reconciliation ledger records a nested `omit` row for each. The parse still accepts them and derives `collapse` from one only when `collapse` is absent, so a stored entry keeps its meaning, and a `collapse` set in the form outranks any alias it carries.
11+
- `indexes` (Advanced, beside `datasource`) — three sub-rows over the keys the SQL driver reads: `name` (text), `fields` (`widget: 'string-tags'`, required) and `unique`, a select offering **only** `global` and `organization`. The deprecated bare `unique: true` is never offered: a schema-derived control would take the union's first arm and render a switch that writes it. An edit merges into the stored entry, so an index that already carries `true` or `false` keeps it until the author picks a scope, and the select can write only the two values the parse accepts. `type` and `partial` are tombstones and have no row.
12+
13+
The help text states what the runtime does with each value. `indexes[].fields` is free text, and no authoring door judges its names: not the schema parse, not the publish door, not `os validate`. A name that is not a stored column makes the SQL driver skip the whole index at sync with a warning in the server log, and the help text says exactly that. A field group has no field-name list: a field joins a group through its own `group` key.
14+
15+
The two row schemas also carry a JSON Schema `title` on every property, as every repeater row schema must: `IndexSchema` on `name`, `fields` and `unique`, and `ObjectFieldGroupSchema` on its nine keys, the three deprecated aliases included. A property panel that reads the served schema's titles therefore shows a named column instead of a raw key. Each title is a `.meta({ title })` call and nothing more.
16+
17+
⛔ **No schema accept set moves and no export changes.** `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves (its rows, and the titles above in its JSON Schema) and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles. Their 22 new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills.
18+
19+
⛔ **The gate that would notice a missing row is NOT landed here.** The reconciliation gate's top-level `zodOnly` direction stays unwired; this change lands offers and three nested ledger rows only.

‎.changeset/20106-reclaim-space-full-freelist.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,4 @@ Clause-②: no
1414

1515
`SqliteWasmDriver` was already complete: its dialect steps every PRAGMA to the end (300 → 0 before and after this change).
1616

17-
On a file-backed database in WAL mode (the default) the database file shrinks once a checkpoint runs, and during the call the freed pages pass through the `-wal` file, which keeps its size until the last connection closes.
18-
1917
Nothing to migrate: `reclaimSpace()` keeps its signature, and a database whose `auto_vacuum` mode is not `INCREMENTAL` still reclaims nothing, as before.

‎.changeset/20203-epoch-ms-date-comparand.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ The rule is shared by the drivers' write and read paths too:
2828
- `create()` / `update()` on either driver, given a number for a `date` field, stores its UTC day. Before, `driver-memory` and SQLite stored the number, and PostgreSQL refused the statement. The engine and REST write doors refuse a number on a `date` field before a driver sees it (`VALIDATION_FAILED`), as before.
2929
- A number already stored in a SQLite `date` column is read back as its UTC day by `find()`, a `groupBy` key and `distinct()`. Only a direct driver write could have put one there.
3030

31-
Not changed, measured identical before and after: `NaN`, ±Infinity, a number outside the `Date` range (past ±8.64e15), a bigint, an epoch-millisecond string, every `Date` and every string on a `date` field (#20240, in the same release, then pads a `Date`'s or a number's year 0..999 to four digits and refuses one whose year falls outside 0..9999, a number past the `Date` range included), and every `datetime` and `time` reading. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here.
31+
Not changed, measured identical before and after: `NaN`, ±Infinity, a number outside the `Date` range (past ±8.64e15), a bigint, an epoch-millisecond string, every `Date` and every string on a `date` field (#20240, in the same release, then pads a `Date`'s or a number's year 0..999 to four digits and refuses one whose year falls outside 0..9999, a number past the `Date` range included; #20264, in the same release, narrows that to 0001..9999, so year 0 is refused rather than padded), and every `datetime` and `time` reading. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here.

‎.changeset/20240-date-year-four-digits.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ What changes:
3535

3636
**Fix.** Compare against a `YYYY-MM-DD` day, or a number or `Date` whose UTC calendar day falls in a four-digit year.
3737

38-
**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through the engine and REST: every `datetime` and `time` cell, the same numbers included; every string comparand on a `date` field; every number and `Date` in the years 1000 to 9999; `NaN`, ±Infinity and an Invalid Date, which name no year and are not judged; and every read-path presentation on those three. On MySQL, measured at the driver door, a stored year from 100 to 999 now reads back padded (`0999-06-15`, where it read `999-06-15`); a stored year below 100 read back a century late (`0009-03-04` as `1909-03-04`, mysql2's `Date.UTC` reading of a `DATE`), which this change does not touch and #20280, in the same release, corrects by reading a MySQL `DATE` as its text. `having` reaches the same door in the same release (#20263), so a number or `Date` outside 0..9999 is refused there too. `service-analytics`' raw-SQL decline reads a time dimension by the `datetime` rule, so its answer does not move. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here.
38+
**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through the engine and REST: every `datetime` and `time` cell, the same numbers included (#20264, in the same release, then narrows the range to 0001..9999 on `date` and `datetime` alike: year 0 is refused too, and so is a `datetime` number, `Date` or string outside it, and the padding covers 0001..0999); every string comparand on a `date` field; every number and `Date` in the years 1000 to 9999; `NaN`, ±Infinity and an Invalid Date, which name no year and are not judged; and every read-path presentation on those three. On MySQL, measured at the driver door, a stored year from 100 to 999 now reads back padded (`0999-06-15`, where it read `999-06-15`); a stored year below 100 read back a century late (`0009-03-04` as `1909-03-04`, mysql2's `Date.UTC` reading of a `DATE`), which this change does not touch and #20280, in the same release, corrects by reading a MySQL `DATE` as its text. `having` reaches the same door in the same release (#20263), so a number or `Date` outside 0..9999 is refused there too. `service-analytics`' raw-SQL decline reads a time dimension by the `datetime` rule, so its answer does not move. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here.

‎.changeset/20263-having-temporal-comparand-door.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ The refusal follows the `where` door's words. It names the `having` path, the co
3838

3939
**Fix.** Compare a `date` column with a `YYYY-MM-DD` day, a `datetime` column with an ISO-8601 instant, a bare day or epoch milliseconds, either one with a relative-date placeholder the resolver knows (`{30_days_ago}`, `{current_month_start}`; `having` resolves them from the same release, #20334), and a `time` column with an `HH:MM` or `HH:MM:SS` wall clock.
4040

41-
**Unchanged**, measured identical before and after on the three drivers, both paths and both doors: every `where` and per-aggregation `filter` answer; every `having` on a temporal column whose comparand the rule reads (a `YYYY-MM-DD` day, an ISO instant, an epoch-millisecond number or string, an in-range `Date`, a zone-naive instant, a wall clock, an extended-year instant on a `datetime` column, which that rule reads); `{today}`-style placeholders, known or not; the empty and the whitespace-only string; `null`, `$exists`, `$in` / `$nin`, `$between`, `$not` / `$or` / `$and` and `{ $field }` references; `$contains` and `$startsWith`; every `count` / `sum` / `avg` column, a string comparand included; a `month` bucket; and every existing `having` refusal, in its words.
41+
**Unchanged**, measured identical before and after on the three drivers, both paths and both doors: every `where` and per-aggregation `filter` answer; every `having` on a temporal column whose comparand the rule reads (a `YYYY-MM-DD` day, an ISO instant, an epoch-millisecond number or string, an in-range `Date`, a zone-naive instant, a wall clock, an extended-year instant on a `datetime` column, which that rule reads, until #20264, in the same release, refuses a `datetime` year outside 0001..9999 through the same predicate); `{today}`-style placeholders, known or not; the empty and the whitespace-only string; `null`, `$exists`, `$in` / `$nin`, `$between`, `$not` / `$or` / `$and` and `{ $field }` references; `$contains` and `$startsWith`; every `count` / `sum` / `avg` column, a string comparand included; a `month` bucket; and every existing `having` refusal, in its words.
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/objectql": minor
4+
---
5+
6+
fix(core,objectql)!: a `date` or `datetime` value names a year from 0001 to 9999, or it is refused: `INVALID_FILTER` / 400 as a comparand on `where`, a per-aggregation `filter` and `having`, and `VALIDATION_FAILED` / 400 as a written value (#20264)
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a temporal VALUE at the query door and the write door: no authorable key, spelling or stored shape moves, `packages/spec` is untouched, and a stored row keeps the form it has. What is refused is a day or an instant whose year falls outside 0001..9999, and which in-range year the caller meant is not something a ledger entry can decide. The other categories are closed on facts: the packages publish (not `unpublished`); no ADR-0087 id covers a comparand or a written value (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
11+
12+
**BREAKING**: this narrows what a `date` or `datetime` field accepts, as a filter comparand and as a written value. A value whose year falls outside 0001..9999 used to answer 200 with the wrong rows, 201 with a non-day stored, or a 500 on PostgreSQL; it now answers 400. It ships as `minor` under the launch-window convention for accept-set narrowings.
13+
14+
FROM a `date` or `datetime` value in year 0, before it, or after 9999 (`"+010000-01-01T00:00:00.000Z"`, `"-000001-…"`, `"0000-06-15"`, or the epoch-millisecond number or `Date` of such an instant) → TO `INVALID_FILTER` / 400 as a comparand and `VALIDATION_FAILED` / 400 (`invalid_date`) as a written value. The fix is one line: write a year from 0001 to 9999.
15+
16+
Measured through `engine.find` / `engine.aggregate` / `engine.insert` and `POST /data/:object/query` / `POST /data/:object` (the two doors agree), over seven 2026 rows, `$gt` / `$lt` / `$eq`:
17+
18+
| position | value | before: memory · SQLite · PostgreSQL 16 | now, on all three |
19+
|:--|:--|:--|:--|
20+
| `where` on a `datetime` | year 10000 or −1: a number, `Date` or ISO string | 7/0/0 · 7/0/0 · `DATABASE_ERROR` (500) | `INVALID_FILTER` / 400 |
21+
| per-aggregation `filter`, `having` on `min` of a `datetime` | the same, `$gt` | 7 rows, every group, on all three | `INVALID_FILTER` / 400 |
22+
| `where` on a `datetime` | year 0, every spelling | 7/0/0 · 7/0/0 · 500 | `INVALID_FILTER` / 400 |
23+
| `where` on a `date` | year 0: a number, `Date`, ISO string or bare `0000-06-15` | 7/0/0 · 7/0/0 · 500 | `INVALID_FILTER` / 400 |
24+
| create a `date` | `"+010000-01-01T00:00:00.000Z"` | 201, read back verbatim (not a day) · the same · 500 | `VALIDATION_FAILED` / 400 |
25+
| create a `date` or a `datetime` | year 0, year −1, year 10000 | 201 · 201 · 500 | `VALIDATION_FAILED` / 400 |
26+
27+
MySQL 8.0 answered the year-10000 and year-−1 cells with a 500 and the year-0 cells like SQLite. A `datetime` in year 10000 spells `+010000-…`, which sorts below every four-digit year as text (its `where` answer was 7/0/0 for `$gt` / `$lt` / `$eq`, where the right answer is 0/7/0); PostgreSQL's `DATE` and `timestamptz` have no year 0 (`22008`). Year 0 was answered right on memory and SQLite and a 500 on PostgreSQL; it is refused everywhere now, one answer on every driver. Each refused query or write now reaches no driver.
28+
29+
What changes:
30+
31+
- `@objectstack/core` exports `isOutsideTemporalYearRange(value, kind)`, the one range both doors ask. The year is the one the kind's storage rule reads: a `datetime`'s UTC year, a `date` string's leading `YYYY-MM-DD` year (otherwise the UTC year of the instant it names), never a `time`'s.
32+
- `isUninterpretableTemporalComparand` is true for a `date` or `datetime` number, `Date` or readable string whose year falls outside 0001..9999; before, it judged only a `date` number or `Date`, against 0..9999. The engine's temporal-comparand door refuses such a comparand on `where` (every verb, both spellings), in a per-aggregation `filter` and on `having`, before any read, in words that name the year range. `IObjectQLEngine.judgeFilter` and `service-analytics`' raw-SQL decline read the same predicate.
33+
- The record validator's `date` / `datetime` arm refuses a value outside the range on insert, update, a multi-row update and `engine.validate`, with the field's `invalid_date` code and its existing message.
34+
- `temporalStorageForm(value, 'date')` pads a `Date`'s or a number's year to four digits for 0001..0999 only; year 0 keeps its unpadded spelling (`0-06-15`) like every other year outside the range. Only a direct driver write, which bypasses both doors, reaches that arm with year 0.
35+
36+
**Who is affected.** A caller that filters on or writes a `date` or `datetime` in year 0, before it, or after 9999. No writer that stores or queries such a year has been measured; the reach is the public query and write doors.
37+
38+
**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through the engine and REST: every year from 0001 to 9999 (the edges 0001-01-01 and 9999-12-31T23:59:59.999Z included) and every 2026 control; every `time` cell; every string the rules could not read before, refused in its existing words, except a `date`-column string whose instant names a year outside 0001..9999 (`+010000-01-01T00:00:00.000Z`, `-000001-…`, an out-of-range epoch-millisecond string), refused with the same code and status on `where`, the per-aggregation `filter` and `having` but now in the year-class words; `NaN`, ±Infinity and an Invalid Date, which name no year; the `datetime` storage rule's own spelling of any instant on the write and read paths. On MySQL 8.0, a `datetime` in years 0001..0099 is still stored right and read back a century late through mysql2's instant parser (`0009-03-04T10:00Z` as `2004-09-03T10:00Z`), which ADR-0053 D-F2 keeps and this change does not touch; from year 0100 up it reads back as written. `driver-mongodb` keeps its own copy of the storage rule and is not changed; both doors sit in the engine, in front of it.

0 commit comments

Comments
 (0)