|
| 1 | +--- |
| 2 | +"@objectstack/service-analytics": patch |
| 3 | +--- |
| 4 | + |
| 5 | +fix(service-analytics): an unrecognised `compareTo.kind` is refused, not answered with a previous-period window under a 200 (#17550) |
| 6 | + |
| 7 | +`shiftRange` had one branch and a fall-through — `previousYear` was named, and |
| 8 | +**everything else** landed in the `previousPeriod` arm. No `default`, no |
| 9 | +exhaustiveness check. So `compareTo: { kind: 'previousQuarter' }` came back as a |
| 10 | +previous-period comparison under an ordinary **200**, and the caller was told |
| 11 | +nothing. The wrong answer is a comparison **window**: a number a dashboard |
| 12 | +renders and a person reads as fact, with no status, header or field in the |
| 13 | +response to distinguish it from a real answer. |
| 14 | + |
| 15 | +`DatasetCompareTo.kind` has only ever declared two values |
| 16 | +(`'previousPeriod' | 'previousYear'`), but `DatasetSelection` is a TypeScript |
| 17 | +interface with no Zod schema anywhere, and `/analytics/dataset/query`'s door |
| 18 | +parses only the seven members the selection shares with `AnalyticsQuery` — |
| 19 | +`compareTo` is one of the four it projects away before its parse, and the route |
| 20 | +forwards the caller's selection to the service untouched. So `kind` was checked |
| 21 | +by `tsc` inside this repo and by nothing at all on the wire. |
| 22 | + |
| 23 | +## FROM → TO |
| 24 | + |
| 25 | +| Input | Was | Now | |
| 26 | +|:--|:--|:--| |
| 27 | +| `compareTo: { kind: 'previousPeriod' }` | the equal-length window before | **unchanged** | |
| 28 | +| `compareTo: { kind: 'previousYear' }` | the same window one year back | **unchanged** | |
| 29 | +| `compareTo: { kind: <anything else> }` | a previous-period window, **200** | `DATASET_INVALID` / **400**, naming the value received and both legal ones | |
| 30 | + |
| 31 | +The fix is to name one of the two declared windows, or drop `compareTo` — which |
| 32 | +is what the refusal says. No accept set widens, no new error code is minted: the |
| 33 | +refusal is the fourth member of the `datasetInvalidError` family |
| 34 | +`resolveCompareDimension` already raises three times for the same document, so it |
| 35 | +arrives at the route through the envelope that route already classifies on. |
| 36 | + |
| 37 | +## Why this is a `patch` |
| 38 | + |
| 39 | +It pulls behaviour back onto the contract the type has always declared, rather |
| 40 | +than narrowing past it: every input `DatasetCompareTo` permits returns |
| 41 | +byte-identical windows, pinned by a control in the same change. What flips from |
| 42 | +200 to 400 is input the declared contract never permitted. The reachable-today |
| 43 | +population for that input was measured on the tree — the dashboard authoring path |
| 44 | +is already doored (`DashboardWidgetSchema` parses the widget's `kind` as a |
| 45 | +`z.enum`, so a third kind cannot arrive through a parsed widget), and no producer |
| 46 | +in this repository sends a third value. What is not enumerable from here is a |
| 47 | +consumer outside it calling the published `shiftRange` export, or posting a |
| 48 | +hand-rolled body to the dataset route; for those, the refusal replaces a wrong |
| 49 | +answer with a located one. |
| 50 | + |
| 51 | +`alignedCompareBucketKey` reads the same two-valued `kind` and deliberately gains |
| 52 | +no refusal of its own: it is not on the package's public surface, and its only |
| 53 | +caller runs `shiftRange` first — both pinned, so exporting it turns the pin red |
| 54 | +rather than silently reopening this defect. |
0 commit comments