From cfcb5f7845cdd0f578b8eaea7163eeb34646ebaf Mon Sep 17 00:00:00 2001 From: George Gebbett Date: Fri, 29 May 2026 16:16:33 +0100 Subject: [PATCH] fix(filter): accept runtime numeric values in filter_by Interpolating a `number` into a filter (e.g. `field:=${n}`) arrives as the template-literal type `${number}`, which the tokenizer's char-split collapsed into a single Head that doesn't extend NumericValue, falling through to `Unknown token: ${number}`. Backtick-quoting type-checked but Typesense rejects it at runtime ("invalid comparator"), so numeric fields had no working path for runtime values. Add a FilterTokenizer branch that treats a `${number}`/`${bigint}` placeholder Head as a NumToken and recurses on the tail. Placed after the literal NumericValue branch so literal digits keep their exact value, and the tail recursion handles continuation clauses, arrays, and ranges. --- src/lexer/filter.ts | 3 ++ tests/filter.test.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/src/lexer/filter.ts b/src/lexer/filter.ts index 8908c6d..688c0bb 100644 --- a/src/lexer/filter.ts +++ b/src/lexer/filter.ts @@ -265,6 +265,9 @@ type FilterTokenizer< > : Head extends NumericValue ? FilterTokenizer[1], Schema, [...Acc, NumToken[0]>]> + : Head extends `${number}` | `${bigint}` ? + // Handle an interpolated number, e.g. a runtime value in `field:=${n}` + FilterTokenizer]> : `Unknown token: ${Head}` : T extends keyof TokenMap ? // Handle case when the last token is a symbol (e.g. "&&") diff --git a/tests/filter.test.ts b/tests/filter.test.ts index 931239c..649fc9a 100644 --- a/tests/filter.test.ts +++ b/tests/filter.test.ts @@ -274,6 +274,96 @@ describe("FilterTokenizer tests", () => { FilterTokenizer<"age != 20", typeof _usersSchema> >().toEqualTypeOf<"Unknown token: !">(); }); + + it("should tokenize a `${number}` interpolation as a num token", () => { + expectTypeOf< + FilterTokenizer<`age:=${number}`, typeof _usersSchema> + >().toEqualTypeOf<[Ident<"age", "int32">, ":=", NumToken<`${number}`>]>(); + }); + + it("should tokenize a `${number}` interpolation followed by a clause", () => { + expectTypeOf< + FilterTokenizer<`age:=${number} && name:John`, typeof _usersSchema> + >().toEqualTypeOf< + [ + Ident<"age", "int32">, + ":=", + NumToken<`${number}`>, + "&&", + Ident<"name", "string">, + ":", + LiteralToken<"John">, + ] + >(); + }); + + it("should tokenize `${number}` interpolations inside a range", () => { + expectTypeOf< + FilterTokenizer<`age:[${number}..${number}]`, typeof _usersSchema> + >().toEqualTypeOf< + [ + Ident<"age", "int32">, + ":[", + NumToken<`${number}`>, + "..", + NumToken<`${number}`>, + "]", + ] + >(); + }); + + it("should tokenize a `${bigint}` interpolation as a num token", () => { + expectTypeOf< + FilterTokenizer<`age:=${bigint}`, typeof _usersSchema> + >().toEqualTypeOf<[Ident<"age", "int32">, ":=", NumToken<`${bigint}`>]>(); + }); + + it("should still tokenize literal numbers with their exact value", () => { + expectTypeOf< + FilterTokenizer<"age:=20", typeof _usersSchema> + >().toEqualTypeOf<[Ident<"age", "int32">, ":=", NumToken<"20">]>(); + }); +}); + +describe("ParseFilter with runtime `${number}` values", () => { + it("should accept a runtime numeric value with `:=`", () => { + expectTypeOf< + ParseFilter<`age:=${number}`, typeof _usersSchema> + >().toEqualTypeOf(); + }); + + it("should accept a runtime numeric value with comparison operators", () => { + expectTypeOf< + ParseFilter<`age:>${number}`, typeof _usersSchema> + >().toEqualTypeOf(); + expectTypeOf< + ParseFilter<`age:<${number}`, typeof _usersSchema> + >().toEqualTypeOf(); + }); + + it("should accept a runtime numeric value inside an array", () => { + expectTypeOf< + ParseFilter<`age:[${number}]`, typeof _usersSchema> + >().toEqualTypeOf(); + }); + + it("should accept a runtime numeric range", () => { + expectTypeOf< + ParseFilter<`age:[${number}..${number}]`, typeof _usersSchema> + >().toEqualTypeOf(); + }); + + it("should accept a runtime numeric value followed by another clause", () => { + expectTypeOf< + ParseFilter<`age:=${number} && name:John`, typeof _usersSchema> + >().toEqualTypeOf(); + }); + + it("should accept a runtime bigint value", () => { + expectTypeOf< + ParseFilter<`age:=${bigint}`, typeof _usersSchema> + >().toEqualTypeOf(); + }); }); describe("IsNextTokenValid type tests", () => {