|
| 1 | +--- |
| 2 | +'@objectstack/rest': minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(rest): `POST /api/v1/data/:object/import` reads a `date`, `datetime` or `time` cell only in ISO 8601 or the export's own `YYYY-MM-DD HH:mm:ss`, on a calendar day that exists, and keeps a `date`'s year at four digits (#20534) |
| 6 | + |
| 7 | +Clause-②: no (narrowing) |
| 8 | + |
| 9 | +**BREAKING for callers of the import door.** A text cell for a `date`, |
| 10 | +`datetime` or `time` field is now read only in one of these spellings, after |
| 11 | +trimming: |
| 12 | + |
| 13 | +- `YYYY-MM-DD`; |
| 14 | +- `YYYY-MM-DDTHH:MM[:SS[.fraction]]`, then `Z`, a `+HH:MM` / `-HH:MM` / |
| 15 | + `+HHMM` offset, or nothing (a wall clock, read in the importing user's |
| 16 | + business timezone, as before); |
| 17 | +- `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no offset, which is what the export |
| 18 | + writes for a `datetime` cell; |
| 19 | +- for a `time` field, also a bare `HH:MM` / `HH:MM:SS`. |
| 20 | + |
| 21 | +The day must exist. Every other cell is that row's `invalid_date` error, with |
| 22 | +the importer's existing sentence ("is not a valid date" / "datetime" / |
| 23 | +"time"). The reader used to hand such a cell to the JavaScript date parser, |
| 24 | +which read it in the SERVER PROCESS's timezone and month-first, and rolled an |
| 25 | +impossible day into the next month, so the import reported success and stored |
| 26 | +a different value. For each shape, change the cell FROM the refused spelling |
| 27 | +TO the ISO one: |
| 28 | + |
| 29 | +- **An impossible day.** FROM `2026-02-30`, `2026-02-29`, `2026-04-31` in any |
| 30 | + spelling (a `datetime` `2026-02-30` was stored as 2 March, and so was a |
| 31 | + `date` written `2026-02-30T10:00:00Z`) TO the day you mean. Nothing is |
| 32 | + rolled over. |
| 33 | +- **A locale or prose date.** FROM `07/15/2026`, `07/15/2026 10:00`, |
| 34 | + `07/08/2026`, `15 July 2026`, `Jul 15 2026 10:00` (stored hours apart on a |
| 35 | + New York and a Shanghai server, a `date` a day apart, and `07/08/2026` read |
| 36 | + as 8 July) TO `2026-07-15`, `2026-07-15 10:00`, `2026-07-08` or |
| 37 | + `2026-08-07`. No timezone and no field order is guessed. Converting a |
| 38 | + spreadsheet column to ISO (in Excel, the cell format `yyyy-mm-dd` or |
| 39 | + `yyyy-mm-dd hh:mm:ss`) before export is the fix. |
| 40 | +- **A year-first spelling that is not ISO.** FROM `2026/7/15`, `2026/07/15`, |
| 41 | + `2026-7-15`, `2026/07/15 10:00` or a one-digit hour (`2026-07-15 9:00`) TO |
| 42 | + the zero-padded, dash-separated `2026-07-15`, `2026-07-15 10:00`, |
| 43 | + `2026-07-15 09:00`. These used to import as the day they denote; they are |
| 44 | + refused with the rest because the reader accepts ISO 8601 and the export's |
| 45 | + own shape and nothing else. |
| 46 | +- **A zone after a space, or lower-case `t` / `z`.** FROM |
| 47 | + `2026-07-15 10:00Z`, `2026-07-15 10:00:00+08:00`, `2026-07-15t10:00:00z` TO |
| 48 | + `2026-07-15T10:00Z`, `2026-07-15T10:00:00+08:00`, `2026-07-15T10:00:00Z`, |
| 49 | + the spellings the create and update doors take. |
| 50 | +- **A zone-naive `24:00`.** FROM `2026-07-15 24:00` (read in the server's |
| 51 | + zone) TO `2026-07-16 00:00`. `2026-07-15T24:00:00Z`, which names its |
| 52 | + instant, reads as before. |
| 53 | +- **A number, reduced or expanded forms.** FROM a JSON number such as `2026` |
| 54 | + or an Excel serial, `2026`, `2026-07`, `+002026-07-15` TO `2026-01-01`, |
| 55 | + `2026-07-01`, `2026-07-15`. |
| 56 | + |
| 57 | +**The year keeps four digits.** A `date` cell for a year from 0001 to 0999 |
| 58 | +(`0500-01-01`) used to leave the reader as `500-01-01`, which the write door |
| 59 | +refuses, so the import refused a day the create door takes. It is stored as |
| 60 | +written now. A bare day read into a `datetime` field in years 0001 to 0099 |
| 61 | +(`0050-01-01`) used to be stored in the 1900s (`1950-01-01T00:00:00.000Z`); it |
| 62 | +is stored in its own year now. |
| 63 | + |
| 64 | +**What is not affected.** Every ISO 8601 cell and every export-shape cell on |
| 65 | +a real day reads exactly as before, including a zone-naive cell read in the |
| 66 | +business timezone and an offset-bearing cell honoured as written. An xlsx |
| 67 | +cell that Excel stores as a date is unaffected: it reaches the reader as the |
| 68 | +export shape. The dry run answers the same verdicts as the real write. A |
| 69 | +refused cell fails only its own row, and the rest of the batch imports as |
| 70 | +before. The same narrowing applies to the exported `coerceRow` helper. |
| 71 | + |
| 72 | +**If you are refused.** The row's result carries `code: 'invalid_date'` and |
| 73 | +quotes the cell, so the file can be corrected and re-imported. |
| 74 | + |
| 75 | +<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row. The import door's cell reader accepts fewer spellings of a date inside an imported file, which is data, not metadata, so `objectstack migrate meta` has nothing to reach. The row's invalid_date error quotes the refused cell, and the repair is to write the date in ISO 8601. --> |
0 commit comments