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
27 changes: 27 additions & 0 deletions .changeset/7265-data-objectstack-filter-operator-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
---

Internal only, no release: `@object-ui/data-objectstack` stops declaring a
module-local `normalizeFilterOperator`, the exact name `@objectstack/spec/ui`
exports, and calls it `toAstFilterOperator` instead (objectui#7265, the
`@object-ui/data-objectstack` slice of the DEBT ledger in
`scripts/check-spec-symbol-derivation.mjs`).

The route is RENAME rather than BIND, and it was decided by measurement against
the RESOLVED 17.4.0 pin rather than by the name. The spec's function folds an
authored spelling to the canonical VIEW vocabulary so `ViewFilterRuleSchema`'s
enum can judge it; this package's translates the same input into the server's
filter-AST symbols, so the spec answers `equals` and `before` where this one
answers `=` and `<`. Same input, different codomain: swapping it in would have
changed what goes on the wire for most of the operators a stored view can carry.

Two things the measurement overturned, both now pinned. The `?? op` tail is not
where the two differ — both hand an unrecognised string back unchanged, so the
lenient tail is common ground. The tail does diverge on the other arm, and in the
opposite direction: the spec returns a NON-string verbatim where this package
returns `null`, and `objectFilterEntryToAST` reads that `null` into a
`MalformedFilterError`. Binding the spec would therefore have widened what this
adapter accepts onto the wire, not tightened it.

The declaration was never exported and no behaviour moved with the name, so
nothing published changes and no package is released by this change.
73 changes: 70 additions & 3 deletions packages/data-objectstack/src/filter-operator-ast-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@
* Adapter operator table → filter-AST parity (#2901, objectstack#3948, #3641).
*
* `FILTER_OPERATOR_ALIASES` is the last translation a filter passes through
* before it goes on the wire, and `normalizeFilterOperator` ends in `?? op` —
* before it goes on the wire, and `toAstFilterOperator` ends in `?? op` —
* an unmapped operator is emitted verbatim. The server then rejects the shape
* at `isFilterAST()`, passes the array through unconverted, and driver-sql
* skips it entirely: **no WHERE clause, no error, every row returned.**
*
* (`toAstFilterOperator` was spelled `normalizeFilterOperator` until
* objectui#7265 renamed it: `@objectstack/spec/ui` exports a DIFFERENT function
* under that name, one that folds to the canonical VIEW vocabulary rather than
* to the AST symbols this table produces. The rename is pinned in
* `scripts/__tests__/spec-symbol-ledger-data-objectstack-7265.test.ts`.)
*
* So a missing row in this table is not a validation failure, it is an
* unfiltered query. `before`/`after` — canonical members of the spec's
* `VIEW_FILTER_OPERATORS` — were missing, which is exactly how a stored
Expand Down Expand Up @@ -48,7 +54,7 @@
*/
import { describe, it, expect } from 'vitest';
import { VALID_AST_OPERATORS } from '@objectstack/spec/data';
import { VIEW_FILTER_OPERATORS } from '@objectstack/spec/ui';
import { VIEW_FILTER_OPERATORS, normalizeFilterOperator } from '@objectstack/spec/ui';
import { FILTER_OPERATOR_ALIASES } from './index';

/**
Expand Down Expand Up @@ -106,7 +112,7 @@ describe('FILTER_OPERATOR_ALIASES lands inside the spec AST vocabulary', () => {
});

it('has a mapping row for every canonical view operator the spec defines', () => {
// Resolution mirrors `normalizeFilterOperator` — lowercased spelling first,
// Resolution mirrors `toAstFilterOperator` — lowercased spelling first,
// then the operator as written — but stops short of its `?? op` tail. That
// tail is production behaviour and must stay there; reproducing it HERE is
// what cancelled this assertion (#3641), because the value it falls back to
Expand All @@ -133,3 +139,64 @@ describe('FILTER_OPERATOR_ALIASES lands inside the spec AST vocabulary', () => {
expect(FILTER_OPERATOR_ALIASES.after).toBe('>');
});
});

/**
* This table is NOT the spec's `normalizeFilterOperator` (objectui#7265).
*
* The local fold over this table was called `normalizeFilterOperator` until that
* card renamed it `toAstFilterOperator`, because `@objectstack/spec/ui` exports a
* function of that name and most of this monorepo imports it. The two take the
* same input and land in different vocabularies, which is the whole reason the
* route was RENAME and not BIND — so the difference is measured here, against the
* resolved pin, rather than asserted in a comment. The spec-side half of the same
* measurement (its fold, its `?? op` tail, its lenient non-string arm) is in
* `scripts/__tests__/spec-symbol-ledger-data-objectstack-7265.test.ts`.
*/
describe('this table is a different vocabulary from the spec fold that shares its old name', () => {
/** Resolution as `toAstFilterOperator` does it, minus its `?? op` tail. */
const row = (op: string) => FILTER_OPERATOR_ALIASES[op.toLowerCase()] ?? FILTER_OPERATOR_ALIASES[op];

it('answers an AST symbol where the spec answers a canonical view word', () => {
const viewVocabulary = new Set<string>(VIEW_FILTER_OPERATORS);

// Both spellings of one operator: the spec folds them together onto a view
// word, this table translates them together onto a wire symbol.
for (const spelling of ['eq', 'equals']) {
expect(String(normalizeFilterOperator(spelling))).toBe('equals');
expect(row(spelling)).toBe('=');
}
// …and the two answers are in different vocabularies, not two spellings of
// one. Read off the spec's own list rather than restated.
expect(viewVocabulary.has('equals')).toBe(true);
expect(viewVocabulary.has('=')).toBe(false);
});

it('…and it is most of the vocabulary, not one operator — enumerated, never counted', () => {
// Every canonical view operator this adapter is the bridge for, plus the
// legacy spellings this table carries rows for. The set is DERIVED so it
// cannot go stale, and the assertion is a floor on which members diverge
// rather than a number, per AGENTS.md #9.
const corpus = [
...VIEW_FILTER_OPERATORS.filter((op) => !NOT_THIS_ADAPTERS_JOB.has(op)),
...Object.keys(FILTER_OPERATOR_ALIASES),
];
const agree: string[] = [];
const diverge: string[] = [];
for (const op of new Set(corpus)) {
(String(normalizeFilterOperator(op)) === String(row(op)) ? agree : diverge).push(op);
}

// The divergence is the finding…
expect(diverge).toContain('eq');
expect(diverge).toContain('greater_than');
expect(diverge).toContain('before');
expect(diverge.length).toBeGreaterThan(agree.length);

// …and the agreement is the lit control that keeps it from being vacuous: a
// comparison where NOTHING matched would mean the probe is broken, not that
// the functions differ. `contains` is an identity row on both sides.
expect(agree).toContain('contains');
expect(String(normalizeFilterOperator('contains'))).toBe('contains');
expect(row('contains')).toBe('contains');
});
});
34 changes: 32 additions & 2 deletions packages/data-objectstack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,37 @@ export const FILTER_OPERATOR_ALIASES: Record<string, string> = {
after: '>',
};

function normalizeFilterOperator(op: unknown): string | null {
/**
* Resolve an authored filter operator to the AST symbol the wire takes.
*
* Deliberately NOT named `normalizeFilterOperator`. That name belongs to a
* DIFFERENT function, which `@objectstack/spec/ui` exports and which this
* monorepo's view layer imports from there (`viewFilterFold`,
* `filter-converter`, `ListView`, `UserFilters`, the FilterBuilder). The two
* take the same input and agree on nothing else: the spec's folds a legacy
* spelling to the canonical VIEW vocabulary (`VIEW_FILTER_OPERATORS`) so that
* `ViewFilterRuleSchema`'s enum can judge it, leaving a canonical operator
* unchanged — `eq` becomes `equals` and `before` stays `before`. This one
* translates the same input into the server's filter-AST symbols through
* {@link FILTER_OPERATOR_ALIASES} — `eq` AND `equals` both become `=`, and
* `before` becomes `<`. Which spellings the two disagree on is enumerated by
* the pin named below rather than written down here; it is most of them.
*
* Renamed at objectui#7265, off the ledger in
* `scripts/check-spec-symbol-derivation.mjs`, so that a reader who has seen
* `normalizeFilterOperator` anywhere else in this tree cannot read the call
* below as the same fold. Nothing about the behaviour moved with the name.
*
* The `?? op` tail is shared with the spec's, and load-bearing for the same
* reason: an entry already written in AST form (`'='`, `'nin'`) has no row here
* and must pass through. The `null` arm is NOT shared — the spec's hands a
* non-string back verbatim (its body ends `return op as string`), this one
* refuses, and `objectFilterEntryToAST` turns the refusal into a
* `MalformedFilterError` instead of putting a number in the operator slot of a
* tuple it is about to send. Both halves are pinned, in both directions, in
* `scripts/__tests__/spec-symbol-ledger-data-objectstack-7265.test.ts`.
*/
function toAstFilterOperator(op: unknown): string | null {
if (typeof op !== 'string') return null;
const lower = op.toLowerCase();
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
Expand Down Expand Up @@ -487,7 +517,7 @@ function objectFilterEntryToAST(entry: any): [string, string, any] | null {
// `name`-keyed rule cannot be saved as view metadata in the first place.
const field = entry.field;
const rawOp = entry.operator ?? entry.op ?? '=';
const op = normalizeFilterOperator(rawOp);
const op = toAstFilterOperator(rawOp);
if (!field || !op) return null;
return [String(field), op, entry.value];
}
Expand Down
21 changes: 21 additions & 0 deletions packages/data-objectstack/src/spec-symbol-batch6.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
* MetadataSaveOptions → MetadataClientSaveOptions
* ValidationError → DataApiValidationError
*
* A fourth rename joined the table later, from a different ledger:
* `normalizeFilterOperator` → `toAstFilterOperator` (objectui#7265, rule 1's
* DEBT block in `scripts/check-spec-symbol-derivation.mjs`). It is here because
* the guard a rename needs is this one, whichever ledger sent it; what is
* specific to it -- the site, the block, and the behaviour that refused BIND --
* is in `scripts/__tests__/spec-symbol-ledger-data-objectstack-7265.test.ts`.
*
* The batch's own triage note said "data-objectstack is a direct client of the
* spec protocol, so `SecurityPolicy` / `DroppedFieldsEvent` are probably hand
* copies — derive them first". Half of that held: `DroppedFieldsEvent` was
Expand Down Expand Up @@ -143,6 +150,20 @@ const RENAMES: Array<[local: string, formerly: string, specMeaning: string]> = [
'ValidationError',
'a plain { field, message, code? } entry in a validation report',
],
// objectui#7265's `@object-ui/data-objectstack` slice. Appended here rather
// than given a file of its own: this is the package's spec-symbol parity file
// and the shape it already holds is exactly the one a rename needs. Unlike the
// three above, the renamed symbol is a FUNCTION and a module-local one, so the
// probe above has to see VALUE exports as well as types -- it does, it reads
// the checker's `getExportsOfModule`, and the assertion below would go quiet
// rather than red if it ever stopped.
[
'toAstFilterOperator',
'normalizeFilterOperator',
"folding an authored spelling to the canonical VIEW vocabulary (`eq` -> `equals`), the "
+ '`z.preprocess` step on `ViewFilterRuleSchema.operator` -- NOT a translation to the '
+ 'filter-AST symbols this package emits',
],
];

describe('renamed local concepts do not collide with a spec export', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
* View operator → FilterBuilder operator parity (#2901, #2945).
*
* The third of objectui's spec-operator translation tables. The other two —
* `ListView.mapOperator` and `data-objectstack`'s `normalizeFilterOperator` —
* were pinned to the spec in #2974, which found eight spellings they had missed
* by enumerating instead of deriving. This one maps the same vocabulary onto the
* FilterBuilder's operator ids, and it had missed nine:
* `ListView.mapOperator` and `data-objectstack`'s `toAstFilterOperator` (spelled
* `normalizeFilterOperator` until objectui#7265 renamed it off the spec's own
* export of that name) — were pinned to the spec in #2974, which found eight
* spellings they had missed by enumerating instead of deriving. This one maps
* the same vocabulary onto the FilterBuilder's operator ids, and it had missed
* nine:
*
* not_equals, greater_than, less_than, greater_than_or_equal,
* less_than_or_equal, starts_with, ends_with, is_null, is_not_null
Expand Down
Loading
Loading