|
| 1 | +--- |
| 2 | +"@objectstack/service-analytics": patch |
| 3 | +"@objectstack/rest": patch |
| 4 | +"@objectstack/spec": patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(analytics,rest): five dataset refusals declare `DATASET_INVALID` / 400 themselves, and the route's message-sniffing list shrinks to one entry (#5367) |
| 8 | + |
| 9 | +`POST /analytics/dataset/query` answered `400 DATASET_INVALID` for six error |
| 10 | +families because the route recognised their **prose**, not because the errors |
| 11 | +said anything about themselves. #5352 gave the catch an ADR-0112 envelope branch |
| 12 | +(`error.code` + a 4xx `error.status`, read first) and had to leave a hardcoded |
| 13 | +list of message substrings behind it, since all six producers were still bare |
| 14 | +`throw new Error(…)`: |
| 15 | + |
| 16 | +``` |
| 17 | +/not declared in the dataset|not backed by a declared relationship| |
| 18 | + not supported by the v1 dataset runtime|read-scope-sql| |
| 19 | + not a selected dimension or measure|is not a subset of the selected dimensions/ |
| 20 | +``` |
| 21 | + |
| 22 | +That made the HTTP status of six families a property of their wording. |
| 23 | +Rephrasing `dataset-compiler`'s "is not declared in the dataset's `include`" — |
| 24 | +no logic change — moved that refusal from 400 to 500, i.e. re-opened #5352 for a |
| 25 | +different family, and no test and no gate would have gone red. Prime Directive |
| 26 | +#12 permits an accommodation like that only while it is declared, loud, tested |
| 27 | +**and removable on a schedule**; #5366 delivered the first three and nothing |
| 28 | +carried the fourth. |
| 29 | + |
| 30 | +**Five producers now declare their own verdict.** A new |
| 31 | +`dataset-refusal.ts` in `@objectstack/service-analytics` exports |
| 32 | +`datasetInvalidError` — the same shape as that package's existing |
| 33 | +`invalidFilterError` (`INVALID_FILTER` / 400) and `assertDimensionFields` |
| 34 | +(`INVALID_FIELD` / 400) — and five sites throw through it: |
| 35 | + |
| 36 | +- `dataset-compiler.ts` — a measure whose aggregate the v1 runtime cannot lower; |
| 37 | + a dimension/measure traversing a relationship path the dataset never declared |
| 38 | + in `include`; |
| 39 | +- `dataset-executor.ts` — an `order` key that is not a selected dimension or |
| 40 | + measure; a `totals` grouping that is not a subset of the selected dimensions; |
| 41 | +- `native-sql-strategy.ts` — a join outside the dataset's declared allowlist. |
| 42 | + |
| 43 | +Their five entries are gone from the route's list, which is now a single |
| 44 | +`read-scope-sql` test. |
| 45 | + |
| 46 | +**`read-scope-sql` deliberately stays.** Its ten fail-closed refusals are RLS |
| 47 | +read-scope lowering failures whose inputs are an admin-authored policy and a |
| 48 | +compiler-generated join alias — not caller input — so `DATASET_INVALID` ("your |
| 49 | +request is invalid") may well be the wrong verdict and choosing the right one is |
| 50 | +a separate judgement, still tracked by #5367. Deleting the entry before that |
| 51 | +judgement lands would regress those ten from `400 DATASET_INVALID` to 500. |
| 52 | + |
| 53 | +**No outward behaviour change for the five.** They answered |
| 54 | +`400 DATASET_INVALID` before and answer `400 DATASET_INVALID` now, with the same |
| 55 | +message; what changed is the mechanism, from message-matching to the producer's |
| 56 | +own declaration. The one visible difference is for a bare `Error` that merely |
| 57 | +*resembles* one of those messages: it is no longer promoted to a 400. That is the |
| 58 | +point — a phrase is no longer a classification. |
| 59 | + |
| 60 | +`DATASET_INVALID` is registered in `ERROR_CODE_LEDGER` under |
| 61 | +`@objectstack/service-analytics` as well as `@objectstack/rest` (provenance, per |
| 62 | +ADR-0112 D3; the code itself is unchanged and the union does not grow), and the |
| 63 | +constructor types it as `RegisteredErrorCode` so an unregistered code is a |
| 64 | +compile error rather than a body some route rejects at runtime. |
| 65 | + |
| 66 | +Coverage: `dataset-refusal-envelope.test.ts` (service-analytics) pins each of the |
| 67 | +five refusals against its real producer — the refusal SET first, green before and |
| 68 | +after, then the envelope; `analytics-dataset-refusal-envelope.test.ts` (rest) |
| 69 | +drives all five end-to-end through a real `AnalyticsService` with positive |
| 70 | +controls on both the aggregate and raw-SQL paths; and |
| 71 | +`analytics-filter-refusal-envelope.test.ts` pins the deletion in both directions |
| 72 | +— the five messages answer 400 when enveloped and 500 when bare, so re-adding a |
| 73 | +regex entry turns it red. |
0 commit comments