Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/6864-aggregate-spec-shape-analytics-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
'@object-ui/data-objectstack': minor
---

`aggregate()`'s spec-shape branch now REFUSES the analytics branch's `filter` /
`field` / `function` instead of dropping them (objectui#6864, extending the
maintainer ruling of 2026-08-30 on objectui#6825 — option A, refuse at the
producer).

**Breaking for callers that were already broken, so read this if you call
`aggregate()` with an ARRAY `groupBy`.** The spec-shape branch — entered when
`params` carries an array `groupBy`, an array `aggregations`, or any `where`
key — builds its request from exactly four keys: `groupBy`, `aggregations`,
`where`, `limit`. `filter`, `field` and `function` are the OTHER branch's
parameters, and they were neither read, nor refused, nor warned about: they
were simply absent from the body that went to `POST /data/:object/query`.

**Why that was worse than the `where` half #6825 fixed.** `field` + `function`
are the analytics branch's whole measure, and this branch takes a measure only
out of `aggregations`. So the legacy shape `{ field, function, groupBy, filter }`
whose `groupBy` happened to be an ARRAY produced a query carrying a `groupBy`
and **no aggregations at all** — a grouping with no measure — with the author's
filter gone as well. The chart rendered, the numbers were wrong, and there was
nothing on screen or on the wire to look at.

**What now throws that previously went through.** A spec-shape call carrying a
non-nullish `filter`, `field` or `function` throws the new
`AnalyticsKeysOnSpecShapeError`. It carries the `INVALID_FILTER` / 400 pair its
siblings carry (so `isMalformedFilterError()` recognises it and a failed widget
renders "this filter is malformed" rather than "check your connection"), plus
`keys` — the offending key names — and `received`, what each one carried. The
message names each key, says what its spec-shape equivalent is, states which
`looksLikeSpecShape` disjunct put the call on this branch, and says outright
when the resulting query would have had no measure. Nothing is sent to the
server, so no unfiltered numbers come back.

**What is deliberately NOT refused.** The legacy analytics shape (a STRING
`groupBy`) is untouched and still lowers `filter` and still fuses
`field` + `function` into its measure — the refusal lives inside the spec-shape
branch only. A key that is present but nullish (`filter: undefined`) carries
nothing to drop and passes, so params built by spreading possibly-absent
authored values keep working. And keys outside those three (`orderBy`, a future
spec key, any host extra) are not refused: the gate names three keys, and only
those.

**Migration.** Pick one shape per call. Spec-shape: `{ groupBy: GroupByNode[],
aggregations: AggregationNode[], where?, limit? }`, with `where` already lowered.
Analytics: `{ field, function, groupBy: string, filter? }`, which lowers `filter`
for you. `AnalyticsKeysOnSpecShapeError` is exported from
`@object-ui/data-objectstack`.
51 changes: 48 additions & 3 deletions packages/data-objectstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,45 @@ Two shapes are deliberately NOT refused, because the receiving door accepts
them: a `FilterCondition` object (`{ stage: 'won' }` — what `QuerySchema.where`
declares), and an empty array (`[]` means "no filter").

#### The two shapes are alternatives, not a mixture

The spec-shape branch reads exactly four keys — `groupBy`, `aggregations`,
`where`, `limit` — and `filter` / `field` / `function` are the **analytics**
branch's own parameters. Until objectui#6864 they were neither read nor
reported on the spec-shape branch: they simply were not in the body that went
out. Since #6864 they are refused, by name:

```typescript
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';

declare const dataSource: ObjectStackAdapter;

// ⛔ throws AnalyticsKeysOnSpecShapeError — an ARRAY `groupBy` selects the
// spec-shape branch, and these three keys would have vanished from the query:
// no filter, and no measure at all.
await dataSource.aggregate('opportunity', {
field: 'amount',
function: 'sum',
groupBy: ['stage'],
filter: [{ field: 'stage', operator: 'equals', value: 'won' }],
});

// ✅ the analytics shape, unchanged — a STRING `groupBy` keeps this call on the
// analytics branch, where all three keys are read and `filter` is lowered for you
await dataSource.aggregate('opportunity', {
field: 'amount',
function: 'sum',
groupBy: 'stage',
filter: [{ field: 'stage', operator: 'equals', value: 'won' }],
});
```

Pick one shape per call. A key that is present but nullish (`filter: undefined`)
carries nothing to drop and is not refused, so params built by spreading
possibly-absent authored values keep working. Keys outside those three are not
refused either — the gate names `filter`, `field` and `function`, and only
those.

### Sorting

```typescript
Expand Down Expand Up @@ -434,7 +473,12 @@ import {
// the spec's filter-AST gate rejects (400
// INVALID_FILTER). See "aggregate({ where }) does
// NOT lower" above.
isMalformedFilterError, // Recognises BOTH of the two above, and the server's
AnalyticsKeysOnSpecShapeError, // aggregate()'s spec-shape branch was handed the
// ANALYTICS branch's keys (`filter` / `field` /
// `function`), which it does not read (400
// INVALID_FILTER). See "the two shapes are
// alternatives, not a mixture" above.
isMalformedFilterError, // Recognises ALL THREE of the above, and the server's
// own version of the same refusal.
} from '@object-ui/data-objectstack';
```
Expand Down Expand Up @@ -511,8 +555,9 @@ All errors include unique error codes for programmatic handling:
- `CONNECTION_ERROR` - Connection/network error
- `AUTHENTICATION_ERROR` - Authentication failure
- `VALIDATION_ERROR` - Data validation error
- `INVALID_FILTER` - A filter the adapter refuses to send (`MalformedFilterError`,
`UnloweredAggregateWhereError`); matches the data API's own code for the same refusal
- `INVALID_FILTER` - A request the adapter refuses to send (`MalformedFilterError`,
`UnloweredAggregateWhereError`, `AnalyticsKeysOnSpecShapeError`); matches the data
API's own code for the same refusal
- `UNSUPPORTED_OPERATION` - Unsupported operation
- `NOT_FOUND` - Resource not found
- `UNKNOWN_ERROR` - Unknown error
Expand Down
Loading
Loading