Skip to content

Commit 5ce3705

Browse files
os-warrenclaude
andauthored
feat(spec): author DatasetSelectionSchema and parse the whole selection at the analytics dataset door (#19638)
Fixes #17551 Clause-②: yes `Ruling-ref: 5754491527` — decision batch **#204 item 3**, **letter A**, maintainer 「204 同意」 2026-09-21T02:08Z. Governing text: `docs/NORTH-STAR.md` 〈优先级〉4 「错的必须被响亮拒绝并给处方,永不静默落库」. ⛔ This PR executes that ruling; it does not reopen it. Option C (a second declaration in `packages/rest`) was refused by name and is not taken. ## What was wrong `DatasetSelection` was a TypeScript **interface** with no Zod schema anywhere in the repo. PR #17548 (card #17058) doored `POST /api/v1/analytics/dataset/query`, but only over the **seven** members the selection shares with `AnalyticsQuery` — the other **four** (`runtimeFilter`, `dateGranularity`, `compareTo`, `totals`) were declared in TypeScript, published in the api-surface, and enforced by nothing on the wire. #17550 is the measured consequence: `compareTo: { kind: 'nonsense' }` returned a previous-period comparison under an ordinary **200**. ## The placement, and why **`packages/spec/src/api/analytics.zod.ts`.** The ruling left the choice between that file and `contracts/analytics-service.ts` to this seat, ONE place. Three readings decided it: 1. `DatasetSelection`'s own published text calls it 「the wire shape a preview/query endpoint posts」. `api/analytics.zod.ts` is 「the HTTP interface for the Semantic Layer」 and already holds `AnalyticsQueryRequestSchema`, the **sibling routes' request body**. One file now holds the analytics family's request-body declarations. 2. `contracts/analytics-service.ts` is type-only for this service — every one of its analytics imports is `import type`. A Zod value there would put `zod` on the import path of anything that imports `@objectstack/spec/contracts` for types alone. 3. ⭐ Only in the zod tier can the seven shared members be taken **by reference** off `AnalyticsQuerySchema.shape`. The door used to carry that agreement as a hand-written array — a standing *claim* that two declarations matched. Taking the declarations themselves makes it structural: there is no second copy left to drift, and a member that leaves `AnalyticsQuery` fails the build rather than becoming a private dialect. ⇒ `@objectstack/spec/contracts` **re-exports** the `DatasetSelection` / `DatasetCompareTo` types from the schema instead of declaring interfaces beside it — the move `AnalyticsQuery` already made in that same file (#4538), taken here before a mirror could drift rather than after. ⛔ There is no second declaration anywhere. ## The door's parse point `packages/rest/src/rest-server.ts` → `datasetSelectionRefusal(selection)`, unchanged in position: after the route's own `selection.measures` check and **before** `queryDataset` is called. What changed is what it parses — the whole selection against `DatasetSelectionSchema`, where it used to parse a projection of seven members. The envelope is unchanged (`400 VALIDATION_FAILED` + `details.fields[]`, or the family's `ANALYTICS_DATE_RANGE_UNRECOGNIZED` when every issue is that condition), and the route still forwards the **caller's** object to the service by identity, never a parse output. ⚠️ One live consequence recorded at the code: the shared mapper's root rename was inert under the projection and is live now. A `.strict()` parse puts an unrecognized-keys issue at the ROOT, which the mapper spells `(body)` — true for the sibling routes, false here — so the root is re-spelled `selection`. ## The refusal messages, and their remedies | condition | what the caller is told | |:---|:---| | `compareTo.kind` outside the closed pair | what arrived, the two windows the executor implements (`'previousPeriod'` / `'previousYear'`, each described), 「Name one of those, or drop compareTo」, where it was refused, and that an unrecognised spelling used to answer a comparison under a 200 | | `compareTo.offset` (retired in #5011) | `{ offset: '1y' }` is exactly `previousYear`; for any other duration, state the window on the `timeDimensions[]` entry and compare with `previousPeriod` | | `compareTo: 'previousPeriod'` (the bare-string form) | write `compareTo: { kind: 'previousPeriod' }`; when to add `dimension` | | an unknown key on the selection | the surface named, the key echoed, and the canonical spelling (`where` → `runtimeFilter`, `orderBy` → `order`, `granularity` → `dateGranularity`, `values` → `measures`) | | `cube` / `dataset` / `datasetName` / `previewDrafts` | the wrong-layer pointer: they live one level up, beside `selection` | | `totals: { dimensions: […] }` | `dimensions` is how a total is **reported back**; ask for it as `totals: { groupings: [[…], []] }` | | `dateGranularity` outside the vocabulary | the closed enum | | `runtimeFilter` | ⛔ deliberately **no dataset-only sentence**. It carries the canonical `FilterCondition`, so its refusals are that vocabulary's own — byte-identical to what the sibling body's `where` answers. A second wording here would be exactly the defect the `compareTo.kind` builder avoids. | ⭐ `datasetCompareKindRefusalMessage(input, origin)` is ONE builder for ONE condition, on the `analyticsDateRangeRefusalMessage` pattern: the schema door raises it with `'schema'`, and `@objectstack/service-analytics`' `shiftRange` — a published export reachable in-process by a caller that never posted a body — raises the same sentence with `'runtime'`. The two differ only in the clause that says where the refusal happened, which is the one clause no input can supply. The executor's own refusal is **not** made redundant and is not removed. ## #17550 as a refusal test `packages/rest/src/analytics-dataset-selection-door.test.ts` §6 drives that card's own specimen through the **real route**: 400, `selection.compareTo.kind` in `details.fields[]`, the message carrying `"nonsense"`, both legal kinds and 「drop compareTo」, the `ApiErrorSchema` envelope check, `queryDataset` **never called**, a CONTROL that the same selection with a declared kind still answers 200 with the caller's object by identity, and a pin that the door's sentence is the builder's. ⚠️ **One correction the ruling's execution list deserves, stated rather than silently absorbed:** the ruling says #17550 「closes as covered when this lands」. #17550 was already **closed** (PR #17570, 2026-09-11) — `shiftRange` gained an exhaustive `switch`, ten days before the ruling was written. So this PR does not close it; it closes the half that fix could not reach, at the door, for the route it names. Nothing else in the execution list is affected. ## Generated artefacts Regenerated with the repo's own tooling, ⛔ never by hand: `pnpm --filter @objectstack/spec build` then `check:generated --fix`, which proved 5 of 15 stale and regenerated exactly those — `api-surface/`, `export-origins/`, `declaration-map/`, `content/docs/references/**`, the strictness-ledger counts — then re-checked all five green. `authorable-surface/` and `json-schema.manifest/` were written by the build's own `gen:schema`. Three hand-maintained ledgers moved with them and each is a decision, not a regeneration: `dropped-refinements.baseline.json` gains `api/DatasetSelection → runtimeFilter.lazy` (the same `FilterCondition` projection gap `api/AnalyticsQueryRequest → where.lazy` already records) with its two header counts, and `type-alias-convention.pin.test.ts` gains three isomorphic pins with its count. ## Acceptance notes - `packages/spec/src/ui/dashboard.zod.ts`'s widget `compareTo` and the new `DatasetCompareToSchema` declare the same two members with different prescriptions, deliberately: the widget's point at neighbouring **widget** keys (`options.dateGranularity`, the widget's own `filter`) that do not exist on a wire selection, and it carries authoring-time tombstones the wire never had. They share the vocabulary by construction — `kind`'s refusal is the one builder — and nothing is filed. - `packages/core/src/utils/analytics-date-range.ts`, `packages/services/service-analytics/src/date-range-array-arm.ts` and `packages/spec/src/data/analytics.zod.ts` each carried a sentence asserting the dataset route parses 「the selection's shared members against `AnalyticsQuerySchema.pick(…)`」. Each conclusion still holds and only the mechanism sentence was stale; all three are corrected in place. Comment-only, no publish surface. - `SELECTION_MEMBERS_SHARED_WITH_ANALYTICS_QUERY` is deleted from `packages/rest`. It was module-local with one consumer (its own test), and the claim it encoded is now pinned at the schema by identity. ## Validation Every command's exit code was captured **before** any pipe. | command | exit | reading | |:---|:---|:---| | `pnpm --filter @objectstack/spec typecheck` | 0 | `tsc --noEmit` + scripts + test layer | | `pnpm --filter @objectstack/spec build` | 0 | under the shared verify lock | | `pnpm --filter @objectstack/spec check:generated` | 0 after `--fix` | 5 of 15 proved stale, regenerated, re-checked green | | `pnpm --filter @objectstack/spec test` | 0 | 510 passed / 1 skipped (511 files), 14930 tests | | `pnpm --filter @objectstack/rest test` | 0 | 194 files, 3265 passed / 1 skipped | | `pnpm --filter @objectstack/service-analytics test` | 0 | 113 files / 2411 tests | | `pnpm --filter @objectstack/core test` | 0 | 51 files / 1321 tests | | `pnpm --filter @objectstack/{spec,rest,service-analytics,core} typecheck` | 0 | four packages, one run | | `pnpm check:spec-parsed-alias` | 0 | 787 pinned isomorphic | The gate family was derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against the real change set rather than from a list, and the derived commands were run and reconciled with `--ran`. --- _Generated by [Claude Code](https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent eff0a96 commit 5ce3705

24 files changed

Lines changed: 1314 additions & 277 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/rest": patch
4+
"@objectstack/service-analytics": patch
5+
---
6+
7+
`DatasetSelectionSchema` — the ADR-0021 dataset selection is a Zod declaration now, and `POST /api/v1/analytics/dataset/query` parses the whole selection against it (#17551).
8+
9+
`DatasetSelection` was a TypeScript **interface** with no Zod schema anywhere in the repo. PR #17548 doored that route, but only over the **seven** members the selection shares with `AnalyticsQuery`; the other **four** — `runtimeFilter`, `dateGranularity`, `compareTo`, `totals` — were declared in TypeScript, published in the api-surface, and enforced by nothing on the wire. The measured consequence is #17550: `compareTo: { kind: 'nonsense' }` came back as a previous-period comparison under an ordinary **200**, a number a dashboard renders and a person reads as fact.
10+
11+
- **One declaration, in `packages/spec`.** `DatasetSelectionSchema`, `DatasetCompareToSchema` and `DatasetTotalsSchema` are authored in `api/analytics.zod.ts`, beside the `AnalyticsQueryRequestSchema` the sibling routes parse. `@objectstack/spec/contracts` now **re-exports** the `DatasetSelection` and `DatasetCompareTo` types from that schema instead of declaring interfaces of its own — the same move `AnalyticsQuery` made in #4538, taken here before a mirror could drift.
12+
- **A transcription, not a new contract.** The seven shared members are read straight off `AnalyticsQuerySchema.shape`, so the claim that the two agree is structural rather than a hand-written list; the four dataset-only members are the already-published TypeScript members made executable. No member is added and nothing the interface permitted is refused.
13+
- **Refusals carry a prescription.** An unrecognised `compareTo.kind` answers the sentence `datasetCompareKindRefusalMessage` builds — what arrived, the two windows the executor implements, what to do — and `@objectstack/service-analytics`' `shiftRange` now raises that same sentence with its own origin clause, so one condition keeps one wording. An unknown key is named, echoed and pointed at the canonical spelling (`where` → `runtimeFilter`, `granularity` → `dateGranularity`), and the retired `{ offset }` arm and the pre-#5011 bare-string form each carry their rewrite.
14+
- ⚠️ **What narrows on the wire**, so an upgrading caller can look for it: a selection member whose value the published interface never permitted now answers `400 VALIDATION_FAILED` with `details.fields[]` instead of travelling into the executor. Measured against the sibling route spelling for spelling, `runtimeFilter` now behaves exactly as `/analytics/query`'s `where` does — three structurally-malformed filter spellings (`{ $or: 'x' }`, an `$or` branch that is not a filter object, `{ $not: 5 }`) are refused at the schema on both routes, and the four semantic ones (`{ stage: {} }`, `{ amount: { $between: [10] } }`, `{ $nor: […] }`, `{ $or: [] }`) still pass both and are answered deeper. The dataset route was the looser of the two; it is not any more.
15+
- **No valid selection changes.** Every in-repo specimen and all five `@object-ui` call sites that build a selection today still pass, pinned in both packages; the route still forwards the caller's object to the service by identity, never a parse output, and the schema carries no default or transform that could override the engine's own timezone resolution chain.

‎content/docs/references/api/analytics.mdx‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ Provides endpoints for executing analytical queries and discovering metadata.
1717
## TypeScript Usage
1818

1919
```typescript
20-
import { AnalyticsEndpoint, AnalyticsMetadataResponseSchema, AnalyticsQueryRequestSchema, AnalyticsResultResponseSchema, AnalyticsSqlResponseSchema, GetAnalyticsMetaRequestSchema } from '@objectstack/spec/api';
21-
import type { AnalyticsEndpoint, AnalyticsMetadataResponse, AnalyticsQueryRequest, AnalyticsResultResponse, AnalyticsSqlResponse, GetAnalyticsMetaRequest } from '@objectstack/spec/api';
20+
import { AnalyticsEndpoint, AnalyticsMetadataResponseSchema, AnalyticsQueryRequestSchema, AnalyticsResultResponseSchema, AnalyticsSqlResponseSchema, DatasetCompareToSchema, DatasetSelectionSchema, DatasetTotalsSchema, GetAnalyticsMetaRequestSchema } from '@objectstack/spec/api';
21+
import type { AnalyticsEndpoint, AnalyticsMetadataResponse, AnalyticsQueryRequest, AnalyticsResultResponse, AnalyticsSqlResponse, DatasetCompareTo, DatasetSelection, DatasetTotals, GetAnalyticsMetaRequest } from '@objectstack/spec/api';
2222

2323
// Validate data
2424
const result = AnalyticsEndpoint.parse(data);
@@ -193,6 +193,71 @@ const result = AnalyticsEndpoint.parse(data);
193193
| **traceId** | `string` | optional | |
194194

195195

196+
---
197+
198+
## DatasetCompareTo
199+
200+
### Properties
201+
202+
| Property | Type | Required | Description |
203+
| :--- | :--- | :--- | :--- |
204+
| **kind** | `Enum<'previousPeriod' \| 'previousYear'>` | ✅ | Comparison window: previousPeriod (equal-length, immediately before) or previousYear (the same window one calendar year back) |
205+
| **dimension** | `string` | optional | Time dimension to shift; omit when the selection has exactly one dated time dimension |
206+
207+
208+
---
209+
210+
## DatasetSelection
211+
212+
### Properties
213+
214+
| Property | Type | Required | Description |
215+
| :--- | :--- | :--- | :--- |
216+
| **dimensions** | `string[]` | optional | List of dimensions to group by |
217+
| **measures** | `string[]` | ✅ | List of metrics to calculate |
218+
| **runtimeFilter** | `any` | optional | Presentation-scope filter (canonical Query DSL FilterCondition), ANDed with the dataset's intrinsic filter at render |
219+
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| [string, string] }[]` | optional | Time-bucketed dimensions. Each entry names a dimension, an optional bucket `granularity`, and an optional `dateRange` — a preset name from the closed date-range vocabulary (e.g. `'last_7_days'`) or an explicit `[start, end]` window; an unrecognised string answers `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` instead of silently widening. |
220+
| **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | Presentation-scope date bucketing applied to every selected `date` dimension; an explicit `timeDimensions` entry wins over it, and the dataset dimension's own default is used when neither is set |
221+
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
222+
| **limit** | `number` | optional | |
223+
| **offset** | `number` | optional | |
224+
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`); attaches `<measure>__compare` columns |
225+
| **totals** | `{ groupings: string[][] }` | optional | Server-side marginal aggregates; each grouping is a dimension subset to additionally aggregate by, `[]` being the grand total |
226+
| **timezone** | `string` | optional | |
227+
228+
### Nested Shape: `DatasetSelection.timeDimensions[number]`
229+
230+
| Property | Type | Required | Description |
231+
| :--- | :--- | :--- | :--- |
232+
| **dimension** | `string` | ✅ | |
233+
| **granularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | |
234+
| **dateRange** | `Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| [string, string]` | optional | Time window for this dimension: a date-range PRESET name from the closed vocabulary in `data/date-range-presets.ts` (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days — e.g. `'last_7_days'`), or an explicit `[start, end]` array of ISO dates / `{date-macro}` tokens (e.g. `["2023-01-01", "2023-01-31"]`). Any other string is refused at the schema with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`. |
235+
236+
### Nested Shape: `DatasetSelection.compareTo`
237+
238+
| Property | Type | Required | Description |
239+
| :--- | :--- | :--- | :--- |
240+
| **kind** | `Enum<'previousPeriod' \| 'previousYear'>` | ✅ | Comparison window: previousPeriod (equal-length, immediately before) or previousYear (the same window one calendar year back) |
241+
| **dimension** | `string` | optional | Time dimension to shift; omit when the selection has exactly one dated time dimension |
242+
243+
### Nested Shape: `DatasetSelection.totals`
244+
245+
| Property | Type | Required | Description |
246+
| :--- | :--- | :--- | :--- |
247+
| **groupings** | `string[][]` | ✅ | Dimension subsets to additionally aggregate by, in request order; the empty subset is the grand total |
248+
249+
250+
---
251+
252+
## DatasetTotals
253+
254+
### Properties
255+
256+
| Property | Type | Required | Description |
257+
| :--- | :--- | :--- | :--- |
258+
| **groupings** | `string[][]` | ✅ | Dimension subsets to additionally aggregate by, in request order; the empty subset is the grand total |
259+
260+
196261
---
197262

198263
## GetAnalyticsMetaRequest

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1531 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1534 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with
2020
| Module | Pages | Schemas | Description |
2121
| :--- | ---: | ---: | :--- |
2222
| [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
23-
| [API Protocol](/docs/references/api) | 31 | 441 | REST contracts, endpoints, routing, realtime, batch, discovery. |
23+
| [API Protocol](/docs/references/api) | 31 | 444 | REST contracts, endpoints, routing, realtime, batch, discovery. |
2424
| [Automation Protocol](/docs/references/automation) | 14 | 74 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2525
| [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2626
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3434
| [System Protocol](/docs/references/system) | 34 | 273 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 158 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **195** | **1531** | 14 protocol modules |
36+
| **Total** | **195** | **1534** | 14 protocol modules |
3737

3838
---
3939

@@ -62,13 +62,13 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
6262

6363
## API Protocol
6464

65-
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 441 schemas**
65+
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 444 schemas**
6666

6767
REST contracts, endpoints, routing, realtime, batch, discovery.
6868

6969
| File | Schemas |
7070
| :--- | :--- |
71-
| [`analytics.zod.ts`](/docs/references/api/analytics) | `AnalyticsEndpoint`, `AnalyticsMetadataResponse`, `AnalyticsQueryRequest`, `AnalyticsResultResponse`, `AnalyticsSqlResponse`, `GetAnalyticsMetaRequest` |
71+
| [`analytics.zod.ts`](/docs/references/api/analytics) | `AnalyticsEndpoint`, `AnalyticsMetadataResponse`, `AnalyticsQueryRequest`, `AnalyticsResultResponse`, `AnalyticsSqlResponse`, `DatasetCompareTo`, `DatasetSelection`, `DatasetTotals`, `GetAnalyticsMetaRequest` |
7272
| [`auth.zod.ts`](/docs/references/api/auth) | `AuthProvider`, `LoginRequest`, `LoginType`, `RefreshTokenRequest`, `RegisterRequest`, `Session`, `SessionResponse`, `SessionUser`, `UserProfileResponse` |
7373
| [`auth-endpoints.zod.ts`](/docs/references/api/auth-endpoints) | `AuthEndpoint`, `AuthFeaturesConfig`, `AuthProviderInfo`, `DeviceRequestResponse`, `DeviceTokenResponse`, `EmailPasswordConfigPublic`, `GetAuthConfigResponse` |
7474
| [`automation-api.zod.ts`](/docs/references/api/automation-api) | `AutomationApiErrorCode`, `AutomationFlowPathParams`, `AutomationRunPathParams`, `CreateFlowRequest`, `CreateFlowResponse`, `DeleteFlowRequest`, `DeleteFlowResponse`, `FlowSummary`, `GetFlowRequest`, `GetFlowResponse`, `GetRunRequest`, `GetRunResponse`, `ListFlowsRequest`, `ListFlowsResponse`, `ListRunsRequest`, `ListRunsResponse`, `ResumeFailureDetails`, `ToggleFlowRequest`, `ToggleFlowResponse`, `TriggerFlowRequest`, `TriggerFlowResponse`, `UpdateFlowRequest`, `UpdateFlowResponse` |

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ directory rather than per file.
257257
| Dir | Sites |
258258
|---|---|
259259
| `ai/` | 78 |
260-
| `api/` | 451 |
260+
| `api/` | 454 |
261261
| `identity/` | 32 |
262262
| `integration/` | 8 |
263263
| `kernel/` | 247 |

‎packages/core/src/utils/analytics-date-range.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,10 @@ export function resolveAnalyticsDateRangePreset(
235235
* Reachability: on EVERY REST analytics route a schema door parses
236236
* `timeDimensions` ahead of the reader. `POST /analytics/query` and
237237
* `/analytics/sql` parse the whole body; `POST /analytics/dataset/query` has
238-
* parsed its selection's shared members — `timeDimensions` included — against
239-
* `AnalyticsQuerySchema.pick(…)` since PR #17548, the PR that landed that door
240-
* for card #17058 (`rest/src/analytics-selection-door.ts`, wired ahead of the
241-
* executor). So every `dateRange` the union CAN refuse is refused there, with
238+
* parsed its selection's shared members — `timeDimensions` included — since
239+
* PR #17548, and parses the WHOLE selection against
240+
* `DatasetSelectionSchema` since #17551
241+
* (`rest/src/analytics-selection-door.ts`, wired ahead of the executor). So every `dateRange` the union CAN refuse is refused there, with
242242
* the schema's own sentence, and this constructor contributes only its
243243
* `.code`/`.status` to that answer.
244244
*

0 commit comments

Comments
 (0)