Repository navigation
Commit 67c1b11
Fixes #20602
Clause-②: no
## What changes
`GET /api/v1/data/:object/export` wrote a `date` or `datetime` cell's
year unpadded, so a day in the years 0001 to 0999 left the export short
(`500-01-01`, `999-12-31 21:03:58`) and `POST
/api/v1/data/:object/import`, which reads a four-digit year only,
refused the platform's own file.
One source file changes, `packages/rest/src/export-format.ts`, on the
three paths triage named:
- `formatDate`'s `date` branch, `utcWallClock` and `zonedWallClock` now
take the day from one private helper, `calendarDay`.
- **The pad rule: core's `temporalStorageForm`, `date` rule, imported
from `@objectstack/core`** (not mirrored). It pads 0001..0999 and leaves
a year outside 0001..9999 unpadded. `@objectstack/rest` already depends
on `@objectstack/core`; no `packages/core/**` edit.
- `zonedWallClock` no longer reads the year from `Intl`'s `year` part,
which is an ERA year (year 0, 1 BC, reads `1`): padding it would spell
`0001-01-01T03:00:00Z` in New York as `0001-12-31`, a day a year later
than the instant's. The zone's year is the instant's UTC year, plus one
when the zone has reached January while UTC is in December, minus one
the other way round. The day is built with `setUTCFullYear`, never
`Date.UTC`.
- `packages/rest/src/rest-server.ts` is untouched; the pins drive the
real routes.
## This round: merge of `main`, and what it changed for this PR
The seat held this PR behind #20599 (answer `5895522655`, option B).
#20599 has landed (PR #20746, `a6866da0c`), and this round merged
`origin/main` `9509ea106a` into the branch as `c0c254921a` (a merge
commit; no rebase, no force-push; `git merge-tree` was clean).
`main` also carries PR #20843 (`05a7547c9f`, #20280): **a `datetime`
names a year from 1000 to 9999 at both engine doors**, and a `date`
keeps 0001..9999. That moved this PR's own pin: at `c0c254921a` the
route layer of `export-date-year-pad.test.ts` went red (`80 passed | 63
skipped`, every route `beforeAll` failed), because the create door now
refuses the pin's `datetime` rows for 0500 and 0999 (`400
VALIDATION_FAILED`, field `dt`, code `invalid_date`). So the dispatched
step, "drop the `dt: undefined` exclusion for 0001 and 0050", cannot be
taken as written: those rows cannot be created at all. What `81b61a6b18`
does instead:
- **The pin** (`packages/rest/src/export-date-year-pad.test.ts`): the
route rows before year 1000 carry a `date` only; 0099 joins the years
(formatter census and routes); a boundary row pins the one `datetime`
cell the export's padding still reaches at the routes, the instant
`1000-01-01T02:00:00.000Z`, which America/New_York reads on `0999-12-31`
(exported `0999-12-31 21:03:58`, re-imported as the same instant). The
module note says why.
- **Two comments** (one in `export-format.ts`, one in the pin) no longer
say the import "would take" the era-year spelling: after #20280 the
import refuses it, as the write doors do. Comment-only; no behaviour in
`export-format.ts` moved (its blob went `8904e5c4b1b4` to `d318ab58eb98`
on that one comment).
- **The changeset** no longer says a `datetime` `0500-01-01 10:00:00`
re-imports: it names the `date` cell and the zone-boundary `datetime`
cell that do, and says a `datetime` stored before year 1000 exports
padded and is refused by the import, as by the write doors.
## Measured, on the merged tree
Harness: the real `POST /api/v1/data/:object`, `GET
/api/v1/data/:object/export` and `POST /api/v1/data/:object/import`
handlers of a `RestServer` over `ObjectQL` plus `SqlDriver`
(better-sqlite3 `:memory:`) and the real metadata protocol, driven
in-process, business timezone from the resolved `ExecutionContext`, into
a fresh stack for the import. A throwaway probe (deleted, not in the
diff), at `c0c254921a`, CSV, xlsx and JSON, business timezone none /
Asia/Shanghai / America/New_York, host `TZ` unset (UTC) and
`TZ=America/New_York`.
| row | create door | exported cell (none / Shanghai / New York) |
`/import` | stored back |
|:--|:--|:--|:--|:--|
| `date` 0001, 0050, 0099, 0500 (`-01-01`) | 201 | `0001-01-01` etc.,
padded, all zones | ok | identical, 72 of 72 legs |
| `date` 2026 | 201 | `2026-01-01` | ok | identical |
| `datetime` 0001, 0050, 0099, 0500 at 10:00Z | **400**
`VALIDATION_FAILED`, `dt` `invalid_date` (72 of 72) | no row | none |
none |
| `datetime` 1000 at 10:00Z | 201 | `1000-01-01 10:00:00` / `18:05:43` /
`05:03:58` | ok | identical |
| `datetime` 2026 at 10:00Z | 201 | `2026-01-01 10:00:00` / `18:00:00` /
`05:00:00` | ok | identical |
| `datetime` `1000-01-01T02:00:00.000Z` | 201 | `1000-01-01 02:00:00` /
`1000-01-01 10:05:43` / **`0999-12-31 21:03:58`** | ok | identical |
| `datetime` 0050, 0500 at 10:00Z written through the driver (a row
stored before the floor) | not the door | `0050-01-01 10:00:00`,
`0500-01-01 10:00:00` (and zone clocks), padded | row refused, `dt`
`invalid_date` (36 of 36) | nothing stored |
Every row the create door takes round-trips exactly: 144 of 144 legs.
The seat's concern for this landing order, a padded `datetime`
0001..0099 stored 1900 years late with no error, has no path left: the
create door refuses such a `datetime`, and a row stored before the floor
exports padded and is refused loudly by the import, never stored.
**Control, the same probe with `export-format.ts` at `9509ea106a`** (the
merge's `main` parent; blob `5791dbaeb33b` proven on disk, restored to
HEAD `8904e5c4b1b4` with `git diff HEAD` empty): the `date` cells export
`1-01-01`, `50-01-01`, `99-01-01`, `500-01-01`, the New York boundary
cell `999-12-31 21:03:58`, the pre-floor rows `50-01-01 10:00:00`; the
import refuses every one as `invalid_date`. Per format: `ok 4, errors 6`
with no zone and in Asia/Shanghai, `ok 3, errors 7` in America/New_York;
at the head `ok 8, errors 2` everywhere (the two pre-floor rows).
**Ablations at `81b61a6b18`**, each through `node
scripts/ablation-replace.mjs` (anchor hit once, blob moved, restore
proven: blob `d318ab58eb98` equals HEAD and `git diff HEAD` empty); the
subject resolves through `src/` by relative import, so no `dist` leg:
- `calendarDay` returns the unpadded spelling: `111 failed | 61 passed
(172)`; no failing test names 1000, 2026 or 9999; the failures are the
below-1000 formatter cells and every route row whose `date` is before
1000, the boundary row included.
- Only the zone path unpadded (`zonedWallClock` spells the day's
`getUTCFullYear()` unpadded, the era-year correction kept): `14 failed |
158 passed (172)`: the ten zoned formatter cells before 1000
(Asia/Shanghai and America/New_York), the year-boundary pin, and exactly
the boundary route row in America/New_York in CSV, xlsx and JSON. That
is the new route row's `datetime` half biting on its own.
Earlier readings by the predecessor round (head `e739a50fa0`, base
`6981abfd26`), still describing `export-format.ts` as it is: H0 (the
base exported `500-01-01` and `500-01-01 10:00:00` and the import
refused the row) and the one-shot H1 census of 1050 `formatCellValue`
cells (years 1000, 2026, 9999 and +010000 byte-identical except 5 cells
whose zone day is `0999-12-31`, now padded; 0001..0999 padded;
out-of-range zoned cells now spell the rule's year instead of the era
year).
## Tests
`packages/rest/src/export-date-year-pad.test.ts`, 172 tests: the
formatter census (years 0001, 0050, 0099, 0500, 0999, 1000, 2026, 9999;
`date` and `datetime`; zones none / UTC / Asia/Shanghai /
America/New_York / unknown), the 2026 control's exact cells, `Date` and
epoch-ms inputs, the year-boundary pin, and the route round trip per
row, format and business timezone (the xlsx leg also asserts text
cells).
All at `81b61a6b18`, under `scripts/pm/os-verify-lock.sh`:
- `pnpm --filter @objectstack/rest exec vitest run --project local
--maxWorkers=2 src/export-date-year-pad.test.ts
src/import-datetime-year-below-100.test.ts`: `Tests 218 passed (218)`,
host `TZ` unset and again under `TZ=America/New_York`.
- `pnpm --filter @objectstack/rest run test`: `Test Files 239 passed
(239)`, `Tests 4824 passed | 106 skipped (4930)`.
- `pnpm --filter @objectstack/rest run test:repo`: `Tests 8 passed (8)`.
- `pnpm --filter @objectstack/rest run typecheck`: exit 0,
`check:test-typecheck: OK`; `tsc -p tsconfig.test.json --listFilesOnly`
lists the pin file.
- The public surface of `@objectstack/rest` is byte-unchanged
(`calendarDay` is private), so no downstream consumer owes a test.
## Gates
At `81b61a6b18` (merge base `9509ea106a`):
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`: 60 commands, all exit 0. `check:dual-build-cjs-loads` and
`check:type-check-debt` first exited 3 (`PREREQUISITE NOT MET`, no
`dist`) and are green after `pnpm exec turbo run build
--filter='./packages/*' --filter='./packages/*/*' --concurrency=2`.
`--ran`: `60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN`.
- NOT MEASURED locally, by the tool's own account: the six families
whose argv takes a value from the workflow, and the eleven whole-root
families; CI runs them.
- `pnpm lint` (the whole tree, not narrowed): exit 0, 229 s.
## Acceptance notes
1. **The landing order the seat set is met, and the round trip it asked
about is not a case any more.** #20599 has landed, and #20280's floor
means a `datetime` in 0001..0999 is refused at the create door, so no
such row exports. Every row the doors take round-trips exactly (table
above).
2. **`packages/rest/src/import-datetime-year-below-100.test.ts`** (PR
#20746, read-only here): its `padExportYear` step is now a byte-for-byte
no-op on every cell this export writes for a day in 0001..9999, so on
every cell its own round trip exports. Its round trip runs at 1000 and
2026 only (the floor), so it never carried a year below 1000 through the
export; this PR's boundary row is the end-to-end `datetime` check below
1000 in a zone. Not edited.
3. **Out-of-range years in a business timezone.** A `datetime` whose
zone day falls in year 0 or before spells the rule's year (`0-12-31`)
instead of the era year (`1-12-31`). The import refuses both, and the
write doors refuse such years.
4. **Observed, not filed:** the create door refuses a well-formed ISO
`datetime` before year 1000 with the sentence "At must be a valid
datetime (ISO-8601)", which names the spelling rather than the
1000..9999 range; the record validator chose one sentence per kind on
purpose. No carrier.
5. **Not changed, dormant:** `packages/rest/src/import-prepare.ts`
`xlsxDateToNaiveCell` spells an xlsx date cell's year unpadded. An xlsx
date cell is an Excel serial (from 1900, or 1904), and the export writes
text cells, so no workbook reaches it with a year below 1000.
Taken over in this round by session `session_01VvcEokUG1tvVxkceYfR5XB`
(claim `5916503658`); the branch's first two commits are the predecessor
seat's (session `session_local_1d2a197c-c20e-4e90-9be8-413d4d432289`).
---------
Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
1 parent d78a0bd commit 67c1b11
3 files changed
Lines changed: 367 additions & 5 deletions
File tree
- .changeset
- packages/rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
0 commit comments