|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/runtime": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec)!: `timeDimensions[].dateRange`'s string arm closes to the date-range preset vocabulary; any other string is refused with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` (#16041) |
| 7 | + |
| 8 | +<!-- adr-0087: registered analytics-time-dimension-date-range-vocabulary-closed --> |
| 9 | + |
| 10 | +**BREAKING** — an accept-set narrowing on a published analytics contract. |
| 11 | +`AnalyticsQuerySchema.timeDimensions[].dateRange` (and with it the |
| 12 | +`POST /analytics/query` / `/analytics/sql` bodies, `AnalyticsQueryRequestSchema`, |
| 13 | +and the `AnalyticsQuery` type every driver and `AnalyticsService.query` caller is |
| 14 | +typed against) used to accept ANY string. It now accepts exactly the thirteen |
| 15 | +dashboard date-range preset names, derived from `data/date-range-presets.ts` |
| 16 | +(`z.enum(DATE_RANGE_PRESETS)` — the vocabulary's single source of truth since |
| 17 | +#4614, so the two cannot drift), or the unchanged `[start, end]` array arm. |
| 18 | +Shipped as `minor` under the repo's launch-window convention for breaking |
| 19 | +changes; the hand-migration prescription is registered under protocol major 18. |
| 20 | +Maintainer ruling on #16041 (2026-09-06, decision batch #57, option A — |
| 21 | +contract first, 「同意」): 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」. |
| 22 | + |
| 23 | +## What was wrong |
| 24 | + |
| 25 | +The arm was a bare `z.string()` whose only documented example — `"Last 7 days"`, |
| 26 | +in the schema's own comment — was a value no driver could parse. `driver-memory` |
| 27 | +recognised exactly `today` and a case-sensitive `last N <unit>` and fell every |
| 28 | +other string through to a `[range, range]` pseudo-window that (measured through |
| 29 | +mingo, 2026-09-05) matched **every `Date`-typed row**, 2099 included, because a |
| 30 | +`Date` compares above a `String` under BSON cross-type ordering. The SQL |
| 31 | +strategies read the same bare string as a single ISO day. A dashboard asking for |
| 32 | +one week silently got all of history on one backend and one day on the other, |
| 33 | +at HTTP 200 on both. |
| 34 | + |
| 35 | +## What it does now |
| 36 | + |
| 37 | +- The string arm is `AnalyticsDateRangePresetSchema = z.enum(DATE_RANGE_PRESETS)` |
| 38 | + (`today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, |
| 39 | + `this_quarter`, `last_quarter`, `this_year`, `last_year`, `last_7_days`, |
| 40 | + `last_30_days`, `last_90_days`); the schema example is corrected to |
| 41 | + `'last_7_days'`. |
| 42 | +- Any other value raises ONE prescriptive issue at `timeDimensions.N.dateRange` |
| 43 | + (`analyticsDateRangeRefusalMessage`: the value, the vocabulary, the array |
| 44 | + spelling for an explicit window). `@objectstack/spec/data` exports the |
| 45 | + structural predicate `isAnalyticsDateRangeRefusalIssue` for doors. |
| 46 | +- `POST /analytics/query` and `/analytics/sql` answer the ADR-0112 envelope |
| 47 | + **`400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`** — a new `ERROR_CODE_LEDGER` member |
| 48 | + registered under `@objectstack/runtime` — and the analytics service is never |
| 49 | + reached. A body wrong in more places than the `dateRange` stays the generic |
| 50 | + `400 VALIDATION_FAILED` + `details.fields[]`. |
| 51 | + |
| 52 | +## FROM → TO |
| 53 | + |
| 54 | +| you wrote | write instead | |
| 55 | +|:--|:--| |
| 56 | +| `dateRange: 'Last 7 days'` / `'last 7 days'` | `dateRange: 'last_7_days'` | |
| 57 | +| `dateRange: 'Last 30 days'` / `'last 30 days'` | `dateRange: 'last_30_days'` | |
| 58 | +| `dateRange: 'last 3 months'` | `dateRange: 'last_90_days'`, or an explicit `['{90_days_ago}', '{today}']` | |
| 59 | +| `dateRange: '2026-01-20'` (the SQL single-day dialect) | `dateRange: ['2026-01-20', '2026-01-20']` | |
| 60 | +| `dateRange: 'This week'` | `dateRange: 'this_week'` | |
| 61 | +| `dateRange: ['2026-01-01', '2026-01-31']` | unchanged | |
| 62 | + |
| 63 | +Measured in this repository at the ruling: three authored `'Last 7 days'`, all |
| 64 | +in `packages/spec` tests (re-spelled here), and no published dashboard authors |
| 65 | +the string arm at all — the shipped console lowers presets to the array arm |
| 66 | +before querying. The drivers' own refusal of a non-conforming value that reaches |
| 67 | +them in-process (past the schema) is the sibling card #16322, blocked by this |
| 68 | +one; the fenced `service-analytics` fixture that authors the retired bare-ISO |
| 69 | +spelling is that card's to re-triage. |
0 commit comments