Skip to content

fix(filters)!: type a filter value as string or boolean - #134

Merged
Upd4ting merged 1 commit into
mainfrom
fix/boolean-filter-value
Nov 13, 2026
Merged

Upd4ting merged 1 commit into
mainfrom
fix/boolean-filter-value

Conversation

@Upd4ting

Copy link
Copy Markdown
Member

Why

ANT-2463. The published FilterValue = [value: string, mode: string] does not describe what the filters carry. dms's archive filter (packages/dms/src/implementations/dms-base/search-route.ts, archiveFilter) writes the boolean true. Every data-api reader passes the value to the database unchanged: the field's filter, the default filter (proxy.eq and proxy.ne), and the indexed getAll. The type made dms keep a SAFETY: comment and a true as unknown as string.

What

  • FilterValue is [value: string | boolean, mode: string], the narrowest type that covers dms. No reader needs more: the default filter's comparisons and getAll already accept a boolean. FilterFunction's value reads FilterValue[0], so it widens with it. The default filter's own operator type follows. Behaviour is unchanged.
  • docs/8.filters.md says a route may write a boolean value and that every reader passes it unchanged. Its numberFilter and booleanFilter examples now handle string | boolean; the old raw.trim() no longer type-checks. The data-api-interface skill describes FilterValue.
  • New src/tests/components/boolean_filter.test.ts drives Query.List with route-written boolean filters on a table with an @Index() boolean field:
    • archived: [true, "eq"] goes through the indexed getAll (the source's stages hold getAll) and answers the two archived rows;
    • archived: [true, "ne"] goes through the default filter, with no getAll, and answers the other row;
    • pinned: [true, "eq"] goes through the field's own filter.
  • Two custom test filters that parsed value as text now call parseFloat(String(value)).
flowchart LR
  route["route writes [true, mode]"] --> idx{"eq on an indexed field?"}
  idx -- yes --> getAll["table.getAll(true, index)"]
  idx -- no --> f{"field filter"}
  f -- default --> def["proxy.eq / ne(true)"]
  f -- custom --> own["filter(ctx, proxy, key, true, mode)"]
Loading

Compatibility and version

This is source-breaking for one kind of implementer, so I name the version 0.8.0, a minor bump in 0.x. FilterFunction is a function type, so under strictFunctionTypes its value parameter is checked contravariantly. A probe against this head:

src/compat-probe.ts(6,14): error TS2322: Type '(_context: RequestContext & { this: Row; }, proxy: ValueProxy<any>, _key: string, value: string) => ValueProxy<boolean>' is not assignable to type 'FilterFunction<Row>'.
src/compat-probe.ts(7,102): error TS2345: Argument of type 'string | boolean' is not assignable to parameter of type 'string'.
  • Breaks: a filter whose value is annotated string (line 6), and a filter that uses value as text, such as parseFloat(value), even with the type inferred (line 7).
  • Compiles unchanged: a filter that passes value through, an inferred @Filter((…, value) => proxy.eq(value)), and a filter that ignores value.
  • Runtime: nothing changes.
  • dms: dms#888 pins >=0.7.2 <0.8.0, so dms (ANT-1088) moves its pin to >=0.8.0 <0.9.0 with this tarball. dms can then drop the SAFETY: comment and the cast.

Upgrade note for the docs(changelog) PR after the release:

- `FilterValue` is `[value: string | boolean, mode: string]`, and a `FilterFunction` receives `value: string | boolean` ([#134](https://github.com/AntelopeJS/interface-data-api/pull/134)). A route may write a boolean filter value, which reaches the filter unchanged. A custom filter that annotates `value: string` or parses it as text now narrows it first, for example `typeof value === "string" ? value.trim() : ""` or `String(value)`.

SAFETY comments added

None. The suppression budget is unchanged.

Proof

On origin/main, the new test does not type-check: a boolean filter value is refused, which is the bug.

src/tests/components/boolean_filter.test.ts(102,62): error TS2322: Type 'boolean' is not assignable to type 'string | FilterValue'.
src/tests/components/boolean_filter.test.ts(111,62): error TS2322: Type 'boolean' is not assignable to type 'string | FilterValue'.
src/tests/components/boolean_filter.test.ts(120,52): error TS2322: Type 'boolean' is not assignable to type 'string | FilterValue'.

On this head:

  A boolean filter value a route writes
    ✔ reaches the indexed lookup of an equality filter
    ✔ reaches the default filter of a comparison
    ✔ reaches a field's own filter
  3 passing (20ms)

pnpm run checks: 11 passed, 0 failed. pnpm exec tsc -p docs/tsconfig.json passes. The docs/8.filters.md docs-check block, compiled against this head: the old block fails with TS2339: Property 'trim' does not exist on type 'string | boolean', and the new one compiles.

The api-diff differs on one line, value: string → value: boolean | string in the FilterValue tuple, repeated in each of the 20 bundled entries. Runtime is unchanged.

antelopejs-api-diff origin/main
API: differences against origin/main (committed HEAD).
diff --git a/base/_root/exports/root.d.ts b/head/_root/exports/root.d.ts
index 0273053..b48a09c 100644
--- a/base/_root/exports/root.d.ts
+++ b/head/_root/exports/root.d.ts
@@ -205,7 +205,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/components.d.ts b/head/_root/exports/subpath/components.d.ts
index 37c65f0..2145fc3 100644
--- a/base/_root/exports/subpath/components.d.ts
+++ b/head/_root/exports/subpath/components.d.ts
@@ -125,7 +125,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/index.d.ts b/head/_root/exports/subpath/index.d.ts
index 0273053..b48a09c 100644
--- a/base/_root/exports/subpath/index.d.ts
+++ b/head/_root/exports/subpath/index.d.ts
@@ -205,7 +205,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fcomputed.d.ts b/head/_root/exports/subpath/internal%2Fcomputed.d.ts
index 500f664..6b895ac 100644
--- a/base/_root/exports/subpath/internal%2Fcomputed.d.ts
+++ b/head/_root/exports/subpath/internal%2Fcomputed.d.ts
@@ -125,7 +125,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fdefault-filter.d.ts b/head/_root/exports/subpath/internal%2Fdefault-filter.d.ts
index 209f7f5..754977d 100644
--- a/base/_root/exports/subpath/internal%2Fdefault-filter.d.ts
+++ b/head/_root/exports/subpath/internal%2Fdefault-filter.d.ts
@@ -124,7 +124,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fdeferred-joined-stream.d.ts b/head/_root/exports/subpath/internal%2Fdeferred-joined-stream.d.ts
index 56cb5cc..997e132 100644
--- a/base/_root/exports/subpath/internal%2Fdeferred-joined-stream.d.ts
+++ b/head/_root/exports/subpath/internal%2Fdeferred-joined-stream.d.ts
@@ -122,7 +122,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fextractors.d.ts b/head/_root/exports/subpath/internal%2Fextractors.d.ts
index 0946883..d826698 100644
--- a/base/_root/exports/subpath/internal%2Fextractors.d.ts
+++ b/head/_root/exports/subpath/internal%2Fextractors.d.ts
@@ -122,7 +122,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Ffield-types.d.ts b/head/_root/exports/subpath/internal%2Ffield-types.d.ts
index 24e6da8..d599572 100644
--- a/base/_root/exports/subpath/internal%2Ffield-types.d.ts
+++ b/head/_root/exports/subpath/internal%2Ffield-types.d.ts
@@ -124,7 +124,7 @@ export type FilterFunction<T extends Record<string, any>, U extends Record<strin
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 export type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 export interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Ffilters.d.ts b/head/_root/exports/subpath/internal%2Ffilters.d.ts
index f120890..ee8d029 100644
--- a/base/_root/exports/subpath/internal%2Ffilters.d.ts
+++ b/head/_root/exports/subpath/internal%2Ffilters.d.ts
@@ -2,7 +2,7 @@ export declare function copyFilter(filter: FieldFilter): FieldFilter;
 export {};
 type FieldFilter = FilterValue | FilterValue[];
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 export declare function FilterValuesOf(filter: FieldFilter): FilterValue[];
diff --git a/base/_root/exports/subpath/internal%2Fforeign.d.ts b/head/_root/exports/subpath/internal%2Fforeign.d.ts
index 1e6cc7f..0e27168 100644
--- a/base/_root/exports/subpath/internal%2Fforeign.d.ts
+++ b/head/_root/exports/subpath/internal%2Fforeign.d.ts
@@ -122,7 +122,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 export declare function Foreign(db: SchemaInstance<any>, meta: DataAPIMeta, query: Stream<any>, pluck?: Set<string>): Stream<any>;
diff --git a/base/_root/exports/subpath/internal%2Fjoined.d.ts b/head/_root/exports/subpath/internal%2Fjoined.d.ts
index d60caf4..dca7771 100644
--- a/base/_root/exports/subpath/internal%2Fjoined.d.ts
+++ b/head/_root/exports/subpath/internal%2Fjoined.d.ts
@@ -124,7 +124,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Flist.d.ts b/head/_root/exports/subpath/internal%2Flist.d.ts
index 5e5844d..67626ef 100644
--- a/base/_root/exports/subpath/internal%2Flist.d.ts
+++ b/head/_root/exports/subpath/internal%2Flist.d.ts
@@ -124,7 +124,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fproperties.d.ts b/head/_root/exports/subpath/internal%2Fproperties.d.ts
index f88cb88..db48342 100644
--- a/base/_root/exports/subpath/internal%2Fproperties.d.ts
+++ b/head/_root/exports/subpath/internal%2Fproperties.d.ts
@@ -124,7 +124,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fread-properties.d.ts b/head/_root/exports/subpath/internal%2Fread-properties.d.ts
index cabe594..a173164 100644
--- a/base/_root/exports/subpath/internal%2Fread-properties.d.ts
+++ b/head/_root/exports/subpath/internal%2Fread-properties.d.ts
@@ -123,7 +123,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/internal%2Fvalidation.d.ts b/head/_root/exports/subpath/internal%2Fvalidation.d.ts
index 0e60ba3..09c1052 100644
--- a/base/_root/exports/subpath/internal%2Fvalidation.d.ts
+++ b/head/_root/exports/subpath/internal%2Fvalidation.d.ts
@@ -124,7 +124,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/exports/subpath/metadata.d.ts b/head/_root/exports/subpath/metadata.d.ts
index 844ba45..acd49a1 100644
--- a/base/_root/exports/subpath/metadata.d.ts
+++ b/head/_root/exports/subpath/metadata.d.ts
@@ -134,7 +134,7 @@ export type FilterFunction<T extends Record<string, any>, U extends Record<strin
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 export type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 export declare function FilterValuesOf(filter: FieldFilter): FilterValue[];
diff --git a/base/_root/main.d.ts b/head/_root/main.d.ts
index 0273053..b48a09c 100644
--- a/base/_root/main.d.ts
+++ b/head/_root/main.d.ts
@@ -205,7 +205,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/types.d.ts b/head/_root/types.d.ts
index 0273053..b48a09c 100644
--- a/base/_root/types.d.ts
+++ b/head/_root/types.d.ts
@@ -205,7 +205,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/typesVersions/%2A/components/0.d.ts b/head/_root/typesVersions/%2A/components/0.d.ts
index 37c65f0..2145fc3 100644
--- a/base/_root/typesVersions/%2A/components/0.d.ts
+++ b/head/_root/typesVersions/%2A/components/0.d.ts
@@ -125,7 +125,7 @@ type FilterFunction<T extends Record<string, any>, U extends Record<string, any>
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 interface ForeignJoinedRef {
diff --git a/base/_root/typesVersions/%2A/metadata/0.d.ts b/head/_root/typesVersions/%2A/metadata/0.d.ts
index 844ba45..acd49a1 100644
--- a/base/_root/typesVersions/%2A/metadata/0.d.ts
+++ b/head/_root/typesVersions/%2A/metadata/0.d.ts
@@ -134,7 +134,7 @@ export type FilterFunction<T extends Record<string, any>, U extends Record<strin
     row: ValueProxy<U>
 ]) => ValueProxyOrValue<boolean>;
 export type FilterValue = [
-    value: string,
+    value: boolean | string,
     mode: string
 ];
 export declare function FilterValuesOf(filter: FieldFilter): FilterValue[];
API: skipped .agents/skills/formal-field-access, private.
API: skipped .agents/skills/verify-interface, private.
API: skipped test-helper entry ./dist/*.d.ts in .; test helpers need a test runner and are outside both proof halves.
Runtime API: 1 packages, 24 entries unchanged against origin/main (committed HEAD).
Runtime API: skipped test-helper entry ./dist/*.js in .; test helpers need a test runner and are outside both proof halves.
No behavioural probe ran.

Suppressions in authored code files (origin/main and committed HEAD). A lint directive counts once per rule it names; `*` is a directive naming none.
| Suppressions | base | HEAD |
| --- | ---: | ---: |
| Lint suppressions, all rules | 0 | 0 |
| `@ts-expect-error` | 0 | 0 |
| `@ts-ignore` | 0 | 0 |
| `SAFETY:` comments | 3 | 3 |
| Lint config rule-offs, all rules | 0 | 0 |
<!-- antelopejs-proof base=2def09ca2536774ee513815c9b5ecbd104dd7ebb head=dfafbd775a93c84d1f68f58b463bc922c276fb23 lines=275 digest=639d5d0c72815ff7 command: antelopejs-api-diff origin/main -->

@Upd4ting
Upd4ting merged commit 170f079 into main Nov 13, 2026
10 checks passed
@Upd4ting
Upd4ting deleted the fix/boolean-filter-value branch November 13, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant