You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 76d2fd5
Browse filesBrowse the repository at this point in the historyBrowse files
fix(objectql)!: a per-aggregation `filter` and a `having` refuse a non-boolean `$exists` / `$null` with `INVALID_FILTER` / 400, in the words every driver's `where` refuses it in, instead of reading `$exists` by truthiness and dropping `$null` (#20981)
6
+
7
+
Clause-②: no (narrowing)
8
+
9
+
<!-- adr-0087: not-required (already-registered filter-query-face-comparands-refused-at-save) this narrows the engine's own in-process evaluator to the rule that registered entry already records: its reason states that every query face refuses a non-boolean $null / $exists flag, its surface names a query having, the data-engine aggregate call's having and an aggregation filter among the carriers, and its replacement is this change's whole migration (a flag is the boolean itself; $null true is "has no value", $exists true is "has a value"). This change makes that statement true on the per-aggregation filter and having positions, which the engine evaluates itself after the driver. No authorable key, spelling, export or published type moves, and no stored row is read or rewritten; a stored filter carrying such a flag is already refused when it is saved, by that entry. -->
10
+
11
+
**BREAKING**: this narrows what `aggregate` accepts in two positions, `aggregations[i].filter` and `having`, on every driver. A `$exists` or `$null` comparand that is not a boolean (a string such as `"false"`, a number, `null`, an array) is now refused with `INVALID_FILTER` / 400, before any driver is asked for a row, so an empty table refuses it too, at any depth under `$and` / `$or` / `$not`. A plain object or `undefined` there is refused first by the comparand-type check, in its own words, as before; a `{ $field }` reference there, already refused as a reference outside a scalar comparison, is now refused in this entry's words. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way, per row: it throws the same refusal for a row its per-aggregation filter judges on the flag, with or without a `fields` map (an empty `rows` array, or a row a `$or` branch settles first, is not judged there; `engine.aggregate` judges the whole filter once before any row). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12
+
13
+
**Why a refusal.**`FieldOperatorsSchema` declares both flags as booleans, and every driver's `where` refuses any other comparand. The engine evaluates a per-aggregation `filter` and a `having` itself, and it read one anyway. Measured through `engine.aggregate` on the in-memory driver and on `SqlDriver` (SQLite), with identical answers: `$exists` was read by truthiness, so `"yes"`, `1` and the string `"false"` selected the rows and groups WITH a value, and `0` / `null` the ones without; and `$null` tested only `true` / `false`, so any other value constrained nothing, and every row and every group came back.
14
+
15
+
**What an author sees now.** The message `driver-sql` gives the same flag, beginning `Operator "$exists" on field "FIELD" requires a boolean comparand (true or false).`, naming what arrived and the position (`aggregations[1].filter.stage.$exists`, `having.stage.$null`). Unlike a `where` on `SqlDriver`, the field and the value are not withheld: a per-aggregation `filter` and a `having` never carry a merged read scope.
16
+
17
+
**What to write instead.** Write the boolean itself. `"$exists": true` and `"$null": false` match a field that has a value; `"$exists": false` and `"$null": true` match one that has none.
18
+
19
+
**Who is affected.** A caller that reaches `engine.aggregate` without the REST query door's schema parse (server-side code, a flow or hook, the analytics bridge that lowers a dataset measure's filter into an aggregation filter, a host calling `applyInMemoryAggregation` directly) and read the count as a real answer. `POST /api/v1/data/:object/query` already refused all three positions with 400 `VALIDATION_FAILED` before the request reached the engine, and still does.
20
+
21
+
**Unchanged.**`$exists: true` / `false` and `$null: true` / `false` answer exactly as before, on both positions. `$empty` and every other operator, and `where`.
fix(rest)!: a public form's lookup picker searches and sorts by the first display field the caller may query, so a picker whose first display field is masked for its caller serves its rows instead of answering 403 (#21062)
6
+
7
+
Clause-②: yes (narrowing)
8
+
9
+
<!-- adr-0087: not-required (no-migration-prescription) a change of which display field the public lookup picker (`GET /forms/:slug/lookup/:field`) searches and sorts by: the first display field the caller may query, by the security service's answer, where it used to be the first display field. No authorable key, spelling, export or stored shape moves: `@objectstack/rest` exports nothing new and nothing less, `FormFieldPublicPickerSchema` keeps parsing every value it parsed, and no stored row is read or rewritten. A picker's `displayFields` keep their meaning as the projected fields. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers which field a picker keys on (not `already-registered`); and the change is route behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10
+
11
+
**BREAKING**: this widens what the public lookup picker serves and narrows it in one composition. The narrowing: with a security service that lacks `ISecurityService.getQueryableFields`, or that answers no answer for the object, the picker passes over every display field whose declaration carries a `maskingRule`, for every caller, including a caller the rule is lifted for. So its search and order move to the next display field that declares no rule, and a picker whose display fields all declare a rule is refused `403 PERMISSION_DENIED` without the engine being asked, where it used to be served. The security service this repository ships implements the method, so a deployment using it is not narrowed. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12
+
13
+
**What changed.** The public lookup picker (`GET /forms/:slug/lookup/:field`)
14
+
matches the visitor's search and orders its rows by one key. That key used to
15
+
be the first entry of `publicPicker.displayFields`. It is now the first entry
16
+
the caller may query on, as the security service answers it
17
+
(`ISecurityService.getQueryableFields`). A field whose masking rule applies to
18
+
a caller is served to that caller masked, and the engine refuses to search or
19
+
sort on it with `403 PERMISSION_DENIED`. A picker whose first display field
20
+
declares such a rule therefore answered `403` to every caller the rule applies
21
+
to, on every request. It now serves its rows, sorted and searched on the next
22
+
display field the caller may query. The masked field is still returned in each
23
+
row, masked, as before.
24
+
25
+
**When no display field is queryable** for the caller, the picker answers
26
+
`403 PERMISSION_DENIED` with the engine's refusal for those fields, without
27
+
running a query.
28
+
29
+
**Unchanged.** A picker with no masked display field, and a caller the masking
30
+
rule is lifted for, keep the first display field as the key, with the security
31
+
service this repository ships. A deployment with no security service keeps the
32
+
first display field.
33
+
34
+
**What to do.** Nothing. To choose the field a picker searches when its first
35
+
display field is masked for some of its callers, list a field those callers may
36
+
query among `displayFields`: the first such entry is the one searched and
37
+
sorted on. A picker whose only display fields are masked for its callers is
38
+
refused, so give it one they may query.
39
+
40
+
**What to do after upgrading, if your security service predates `getQueryableFields`.**
41
+
Implement `getQueryableFields` on it: it answers which fields a caller may filter,
42
+
sort, group or aggregate by, and the picker then keys on the first display field
43
+
in that answer. Until it does, give each picker at least one display field that
44
+
declares no `maskingRule`, or the picker is refused for every caller.
0 commit comments