diff --git a/.changeset/auto-register-operators.md b/.changeset/auto-register-operators.md index 8ce3cbfa4..6b4b50e29 100644 --- a/.changeset/auto-register-operators.md +++ b/.changeset/auto-register-operators.md @@ -8,61 +8,75 @@ Each operator and aggregate now bundles its builder function and evaluator facto - **True tree-shaking**: Only operators/aggregates you import are included in your bundle - **No global registry**: No side-effect imports needed; each node is self-contained -- **Custom operators**: Create custom operators by building `Func` nodes with a factory -- **Custom aggregates**: Create custom aggregates by building `Aggregate` nodes with a config +- **Custom operators**: Use `defineOperator()` to create custom operators +- **Custom aggregates**: Use `defineAggregate()` to create custom aggregates +- **Factory helpers**: Use `comparison()`, `transform()`, `numeric()`, `booleanOp()`, and `pattern()` to easily create operator evaluators **Custom Operator Example:** ```typescript -import { - Func, - type EvaluatorFactory, - type CompiledExpression, -} from '@tanstack/db' -import { toExpression } from '@tanstack/db/query' +import { defineOperator, isUnknown } from '@tanstack/db' -const betweenFactory: EvaluatorFactory = (compiledArgs, _isSingleRow) => { - const [valueEval, minEval, maxEval] = compiledArgs - return (data) => { - const value = valueEval!(data) - return value >= minEval!(data) && value <= maxEval!(data) - } -} +// Define a custom "between" operator +const between = defineOperator< + boolean, + [value: number, min: number, max: number] +>({ + name: 'between', + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + if (isUnknown(value)) return null + return value >= minArg(data) && value <= maxArg(data) + }, +}) -function between(value: any, min: any, max: any) { - return new Func( - 'between', - [toExpression(value), toExpression(min), toExpression(max)], - betweenFactory, - ) -} +// Use in a query +query.where(({ user }) => between(user.age, 18, 65)) +``` + +**Using Factory Helpers:** + +```typescript +import { defineOperator, comparison, transform, numeric } from '@tanstack/db' + +// Binary comparison with automatic null handling +const notEquals = defineOperator({ + name: 'notEquals', + compile: comparison((a, b) => a !== b), +}) + +// Unary transformation +const double = defineOperator({ + name: 'double', + compile: transform((v) => v * 2), +}) + +// Binary numeric operation +const modulo = defineOperator({ + name: 'modulo', + compile: numeric((a, b) => (b !== 0 ? a % b : null)), +}) ``` **Custom Aggregate Example:** ```typescript -import { - Aggregate, - type AggregateConfig, - type ValueExtractor, -} from '@tanstack/db' -import { toExpression } from '@tanstack/db/query' +import { defineAggregate } from '@tanstack/db' -const productConfig: AggregateConfig = { - factory: (valueExtractor: ValueExtractor) => ({ +const product = defineAggregate({ + name: 'product', + factory: (valueExtractor) => ({ preMap: valueExtractor, reduce: (values) => { - let product = 1 + let result = 1 for (const [value, multiplicity] of values) { - for (let i = 0; i < multiplicity; i++) product *= value + for (let i = 0; i < multiplicity; i++) result *= value } - return product + return result }, }), valueTransform: 'numeric', -} - -function product(arg: T): Aggregate { - return new Aggregate('product', [toExpression(arg)], productConfig) -} +}) ``` diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index 7c79c459d..86a1f6185 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -1866,6 +1866,95 @@ concat( avg(add(user.salary, coalesce(user.bonus, 0))) ``` +### Custom Operators + +You can define your own operators using the `defineOperator` function. This is useful when you need specialized comparison logic or domain-specific operations. + +```ts +import { defineOperator, isUnknown } from '@tanstack/db' + +// Define a custom "between" operator +const between = defineOperator({ + name: 'between', + compile: ([valueArg, minArg, maxArg]) => (data) => { + const value = valueArg(data) + const min = minArg(data) + const max = maxArg(data) + + if (isUnknown(value)) return null + return value >= min && value <= max + } +}) + +// Use in a query +const adultUsers = createLiveQueryCollection((q) => + q + .from({ user: usersCollection }) + .where(({ user }) => between(user.age, 18, 65)) +) +``` + +You can also use the built-in factory helpers to create operators more concisely: + +```ts +import { defineOperator, comparison, transform, numeric } from '@tanstack/db' + +// Using the comparison helper (handles null/undefined automatically) +const notEquals = defineOperator({ + name: 'notEquals', + compile: comparison((a, b) => a !== b) +}) + +// Using the transform helper for unary operations +const double = defineOperator({ + name: 'double', + compile: transform((v) => v * 2) +}) + +// Using the numeric helper for binary math operations +const modulo = defineOperator({ + name: 'modulo', + compile: numeric((a, b) => b !== 0 ? a % b : null) +}) +``` + +### Custom Aggregates + +Similarly, you can define custom aggregate functions using `defineAggregate`: + +```ts +import { defineAggregate } from '@tanstack/db' + +// Define a "product" aggregate that multiplies all values +const product = defineAggregate({ + name: 'product', + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values) => { + let result = 1 + for (const [value, multiplicity] of values) { + for (let i = 0; i < multiplicity; i++) { + result *= value + } + } + return result + } + }), + valueTransform: 'numeric' +}) + +// Use in a query with groupBy +const categoryProducts = createLiveQueryCollection((q) => + q + .from({ item: itemsCollection }) + .groupBy(({ item }) => item.category) + .select(({ item }) => ({ + category: item.category, + priceProduct: product(item.price) + })) +) +``` + ## Functional Variants The functional variant API provides an alternative to the standard API, offering more flexibility for complex transformations. With functional variants, the callback functions contain actual code that gets executed to perform the operation, giving you the full power of JavaScript at your disposal. diff --git a/package.json b/package.json index 8c802d2a0..26140e9c7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@typescript-eslint/eslint-plugin": "^8.51.0", "@typescript-eslint/parser": "^8.51.0", "@vitejs/plugin-react": "^5.1.2", + "esbuild": "^0.27.2", "eslint": "^9.39.2", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-react": "^7.37.5", diff --git a/packages/db/src/query/builder/aggregates/index.ts b/packages/db/src/query/builder/aggregates/index.ts index e4fc729bb..8a3c00d88 100644 --- a/packages/db/src/query/builder/aggregates/index.ts +++ b/packages/db/src/query/builder/aggregates/index.ts @@ -1,5 +1,5 @@ // Re-export all aggregates -// Importing from here will auto-register all aggregate evaluators +// Each aggregate is a function that creates Aggregate IR nodes with embedded configs export { sum } from './sum.js' export { count } from './count.js' diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index 013b478d2..cfc87c995 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -1,5 +1,5 @@ // Re-export all operators from their individual modules -// Each module auto-registers its evaluator when imported +// Each operator is a function that creates IR nodes with embedded evaluator factories export { eq } from './operators/eq.js' export { gt } from './operators/gt.js' export { gte } from './operators/gte.js' @@ -24,7 +24,6 @@ export { isNull } from './operators/isNull.js' export { isUndefined } from './operators/isUndefined.js' // Re-export all aggregates from their individual modules -// Each module auto-registers its config when imported export { count } from './aggregates/count.js' export { avg } from './aggregates/avg.js' export { sum } from './aggregates/sum.js' diff --git a/packages/db/src/query/builder/operators/add.ts b/packages/db/src/query/builder/operators/add.ts index a6e7fe631..40df1011c 100644 --- a/packages/db/src/query/builder/operators/add.ts +++ b/packages/db/src/query/builder/operators/add.ts @@ -1,29 +1,10 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { CompiledExpression } from '../../ir.js' +import { numeric } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' import type { BinaryNumericReturnType, ExpressionLike } from './types.js' -// ============================================================ -// EVALUATOR -// ============================================================ - -function addEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - return (a ?? 0) + (b ?? 0) - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const addFactory = /* #__PURE__*/ numeric((a, b) => a + b) as EvaluatorFactory export function add( left: T1, @@ -32,6 +13,6 @@ export function add( return new Func( `add`, [toExpression(left), toExpression(right)], - addEvaluatorFactory, + addFactory, ) as BinaryNumericReturnType } diff --git a/packages/db/src/query/builder/operators/and.ts b/packages/db/src/query/builder/operators/and.ts index 525cb547a..6bea67666 100644 --- a/packages/db/src/query/builder/operators/and.ts +++ b/packages/db/src/query/builder/operators/and.ts @@ -1,56 +1,14 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { booleanOp } from './factories.js' +import type { BasicExpression } from '../../ir.js' +import type { ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function andEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - return (data: any) => { - // 3-valued logic for AND: - // - false AND anything = false (short-circuit) - // - null AND false = false - // - null AND anything (except false) = null - // - anything (except false) AND null = null - // - true AND true = true - let hasUnknown = false - for (const compiledArg of compiledArgs) { - const result = compiledArg(data) - if (result === false) { - return false - } - if (isUnknown(result)) { - hasUnknown = true - } - } - // If we got here, no operand was false - // If any operand was null, return null (UNKNOWN) - if (hasUnknown) { - return null - } - - return true - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// AND: short-circuits on false, returns true if all are true +const andFactory = /* #__PURE__*/ booleanOp({ + shortCircuit: false, + default: true, +}) // Overloads for and() - support 2 or more arguments, or an array export function and( @@ -73,7 +31,7 @@ export function and( return new Func( `and`, leftOrArgs.map((arg) => toExpression(arg)), - andEvaluatorFactory, + andFactory, ) } // Handle variadic overload @@ -81,6 +39,6 @@ export function and( return new Func( `and`, allArgs.map((arg) => toExpression(arg)), - andEvaluatorFactory, + andFactory, ) } diff --git a/packages/db/src/query/builder/operators/coalesce.ts b/packages/db/src/query/builder/operators/coalesce.ts index 6bc9642e6..fe9d23dbb 100644 --- a/packages/db/src/query/builder/operators/coalesce.ts +++ b/packages/db/src/query/builder/operators/coalesce.ts @@ -1,23 +1,11 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import type { BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function coalesceEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - return (data: any) => { +// COALESCE returns the first non-null/undefined value +const coalesceFactory: EvaluatorFactory = (compiledArgs) => { + return (data: unknown) => { for (const evaluator of compiledArgs) { const value = evaluator(data) if (value !== null && value !== undefined) { @@ -28,14 +16,10 @@ function coalesceEvaluatorFactory( } } -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - export function coalesce(...args: Array): BasicExpression { return new Func( `coalesce`, args.map((arg) => toExpression(arg)), - coalesceEvaluatorFactory, + coalesceFactory, ) } diff --git a/packages/db/src/query/builder/operators/concat.ts b/packages/db/src/query/builder/operators/concat.ts index f05ff9dc8..892b940f4 100644 --- a/packages/db/src/query/builder/operators/concat.ts +++ b/packages/db/src/query/builder/operators/concat.ts @@ -1,50 +1,26 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import type { BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function concatEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - return (data: any) => { +// CONCAT joins all arguments as strings +const concatFactory: EvaluatorFactory = (compiledArgs) => { + return (data: unknown) => { return compiledArgs .map((evaluator) => { const arg = evaluator(data) - try { - return String(arg ?? ``) - } catch { - try { - return JSON.stringify(arg) || `` - } catch { - return `[object]` - } - } + return String(arg ?? ``) }) .join(``) } } -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - export function concat( ...args: Array ): BasicExpression { return new Func( `concat`, args.map((arg) => toExpression(arg)), - concatEvaluatorFactory, + concatFactory, ) } diff --git a/packages/db/src/query/builder/operators/define.ts b/packages/db/src/query/builder/operators/define.ts new file mode 100644 index 000000000..b851490c6 --- /dev/null +++ b/packages/db/src/query/builder/operators/define.ts @@ -0,0 +1,255 @@ +/** + * Public API for defining custom operators and aggregates. + * + * These functions provide a clean interface for users to create their own + * operators and aggregates without needing to understand the internal IR structure. + */ + +import { Aggregate, Func } from '../../ir.js' +import { toExpression } from '../ref-proxy.js' +import type { + AggregateConfig, + AggregateFactory, + BasicExpression, + CompiledExpression, + EvaluatorFactory, +} from '../../ir.js' +import type { RefProxy } from '../ref-proxy.js' +import type { RefLeaf } from '../types.js' + +// ============================================================ +// EXPRESSION ARGUMENT TYPES +// ============================================================ + +/** + * Represents an argument that can be passed to a custom operator. + * It can be either: + * - A literal value of type T + * - A ref proxy (e.g., `items.value` in a query) + * - An expression that evaluates to T + * + * @example + * ```typescript + * // When you define: defineOperator + * // The operator accepts: + * between(10, 20, 30) // literal values + * between(items.value, 20, 30) // ref proxy + literals + * between(add(a, b), 20, 30) // nested expression + literals + * ``` + */ +export type ExpressionArg = + | T + | RefProxy + | RefLeaf + | BasicExpression + | Func + +/** + * Maps a tuple of types to a tuple of ExpressionArg types. + * Preserves named tuple labels for better IDE experience. + */ +export type ExpressionArgs> = { + [K in keyof T]: ExpressionArg +} + +// ============================================================ +// TYPED COMPILED EXPRESSION TYPES +// ============================================================ + +/** + * A compiled expression that returns a specific type T. + * This is the typed version of CompiledExpression. + * Uses `any` for data to maintain compatibility with CompiledExpression. + */ +export type TypedCompiledExpression = (data: any) => T + +/** + * Maps a tuple of types to a tuple of typed compiled expressions. + * Preserves named tuple labels from TArgs. + * + * @example + * ```typescript + * // If TArgs = [value: number, min: number, max: number] + * // Then CompiledArgsFor = [ + * // value: TypedCompiledExpression, + * // min: TypedCompiledExpression, + * // max: TypedCompiledExpression + * // ] + * ``` + */ +export type CompiledArgsFor> = { + [K in keyof TArgs]: TypedCompiledExpression +} + +/** + * A typed evaluator factory that receives typed compiled args. + * This provides full type safety from operator definition to evaluation. + * + * Note: This type is structurally a subtype of EvaluatorFactory when TArgs + * is a tuple of types that extends Array. The factory helpers cast + * their return type for compatibility. + */ +export type TypedEvaluatorFactory> = ( + compiledArgs: CompiledArgsFor, + isSingleRow: boolean, +) => CompiledExpression + +// ============================================================ +// OPERATOR CONFIG +// ============================================================ + +/** + * Configuration for defining a custom operator. + * + * @typeParam TArgs - A tuple of argument types (can be named tuple) + */ +export interface OperatorConfig = Array> { + /** The name of the operator (used for debugging/serialization) */ + name: string + /** + * The compile function that creates the runtime evaluator. + * Called once during query compilation to produce the per-row evaluator. + * + * You can use: + * - A fully typed compile function: `([a, b, c]) => (data) => ...` with TArgs + * - Factory helpers: `comparison()`, `transform()`, `numeric()`, `pattern()` + */ + compile: TypedEvaluatorFactory +} + +/** + * Configuration for defining a custom aggregate. + */ +export interface AggregateDefinition { + /** The name of the aggregate (used for debugging/serialization) */ + name: string + /** The factory function from @tanstack/db-ivm that creates the aggregate */ + factory: AggregateFactory + /** + * How to transform the input value before aggregation: + * - 'numeric': Coerce to number (for sum, avg) + * - 'numericOrDate': Allow numbers or Date objects (for min, max) + * - 'raw': Pass through unchanged (for count, collect) + */ + valueTransform: AggregateConfig[`valueTransform`] +} + +// ============================================================ +// DEFINE OPERATOR +// ============================================================ + +/** + * Define a custom operator for use in TanStack DB queries. + * + * This function creates a builder function that generates Func IR nodes + * with your custom compile function. The compile function is called once + * during query compilation to produce the per-row evaluator. + * + * @typeParam TReturn - The return type of the operator (default: `unknown`) + * @typeParam TArgs - A tuple of argument types, supports named tuples (default: `unknown[]`) + * @param config - The operator configuration + * @returns A function that creates Func nodes when called with arguments + * + * @example + * ```typescript + * import { defineOperator, isUnknown } from '@tanstack/db' + * + * // Define a fully typed "between" operator with named tuple args + * const between = defineOperator({ + * name: 'between', + * compile: ([value, min, max]) => (data) => { + * // value, min, max are all typed as TypedCompiledExpression + * const v = value(data) + * if (isUnknown(v)) return null + * return v >= min(data) && v <= max(data) + * } + * }) + * + * // In queries, accepts ref proxies AND literals + * query.where(({ user }) => between(user.age, 18, 65)) + * ``` + * + * @example + * ```typescript + * // Using typed factory helpers + * import { comparison, transform } from '@tanstack/db' + * + * const notEquals = defineOperator({ + * name: 'notEquals', + * compile: comparison((a, b) => a !== b) + * }) + * + * const double = defineOperator({ + * name: 'double', + * compile: transform((v) => v * 2) + * }) + * ``` + */ +export function defineOperator< + TReturn = unknown, + TArgs extends Array = Array, +>( + config: OperatorConfig, +): (...args: ExpressionArgs) => Func { + const { name, compile } = config + return (...args: ExpressionArgs): Func => + // Cast is safe because TypedEvaluatorFactory is compatible with EvaluatorFactory + new Func(name, args.map(toExpression), compile as EvaluatorFactory) +} + +// ============================================================ +// DEFINE AGGREGATE +// ============================================================ + +/** + * Define a custom aggregate function for use in TanStack DB queries. + * + * This function creates a builder function that generates Aggregate IR nodes + * with your custom configuration. The aggregate uses the IVM aggregate pattern + * with preMap, reduce, and optional postMap phases. + * + * @typeParam TReturn - The return type of the aggregate (default: `unknown`) + * @typeParam TArg - The semantic type of the argument (default: `unknown`) + * @param config - The aggregate configuration + * @returns A function that creates Aggregate nodes when called with an argument + * + * @example + * ```typescript + * import { defineAggregate } from '@tanstack/db' + * + * // Define a typed "product" aggregate that multiplies all values + * const product = defineAggregate({ + * name: 'product', + * factory: (valueExtractor) => ({ + * preMap: valueExtractor, + * reduce: (values) => { + * let result = 1 + * for (const [value, multiplicity] of values) { + * for (let i = 0; i < multiplicity; i++) { + * result *= value + * } + * } + * return result + * } + * }), + * valueTransform: 'numeric' + * }) + * + * // product accepts: number | RefLeaf | BasicExpression | ... + * query + * .from({ items: itemsCollection }) + * .groupBy(({ items }) => items.category) + * .select(({ items }) => ({ + * category: items.category, + * priceProduct: product(items.price) + * })) + * ``` + */ +export function defineAggregate( + config: AggregateDefinition, +): (arg: ExpressionArg) => Aggregate { + const { name, factory, valueTransform } = config + const aggregateConfig: AggregateConfig = { factory, valueTransform } + return (arg: ExpressionArg): Aggregate => + new Aggregate(name, [toExpression(arg)], aggregateConfig) +} diff --git a/packages/db/src/query/builder/operators/divide.ts b/packages/db/src/query/builder/operators/divide.ts index 8e91163cc..95172d633 100644 --- a/packages/db/src/query/builder/operators/divide.ts +++ b/packages/db/src/query/builder/operators/divide.ts @@ -1,41 +1,13 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { numeric } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' +import type { BinaryNumericReturnType, ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for binary numeric operations (combines nullability of both operands) -type BinaryNumericReturnType<_T1, _T2> = BasicExpression< - number | undefined | null -> - -// ============================================================ -// EVALUATOR -// ============================================================ - -function divideEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - const divisor = b ?? 0 - return divisor !== 0 ? (a ?? 0) / divisor : null - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// Division returns null for division by zero +const divideFactory = /* #__PURE__*/ numeric((a, b) => + b !== 0 ? a / b : null, +) as EvaluatorFactory export function divide( left: T1, @@ -44,6 +16,6 @@ export function divide( return new Func( `divide`, [toExpression(left), toExpression(right)], - divideEvaluatorFactory, + divideFactory, ) as BinaryNumericReturnType } diff --git a/packages/db/src/query/builder/operators/eq.ts b/packages/db/src/query/builder/operators/eq.ts index 56cf19cdc..1d54a256b 100644 --- a/packages/db/src/query/builder/operators/eq.ts +++ b/packages/db/src/query/builder/operators/eq.ts @@ -1,48 +1,17 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' import { areValuesEqual, normalizeValue } from '../../../utils/comparison.js' -import type { - Aggregate, - BasicExpression, - CompiledExpression, -} from '../../ir.js' -import type { RefProxy } from '../ref-proxy.js' -import type { RefLeaf } from '../types.js' +import { isUnknown } from './factories.js' +import type { Aggregate, BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ComparisonOperand } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -type ComparisonOperand = - | RefProxy - | RefLeaf - | T - | BasicExpression - | undefined - | null - -type ComparisonOperandPrimitive = - | T - | BasicExpression - | undefined - | null - -// ============================================================ -// EVALUATOR FACTORY -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function eqEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { +// EQ needs a custom factory because it uses value normalization for proper +// comparison of Dates, BigInts, etc. +const eqFactory: EvaluatorFactory = (compiledArgs) => { const argA = compiledArgs[0]! const argB = compiledArgs[1]! - return (data: any) => { + return (data: unknown) => { const a = normalizeValue(argA(data)) const b = normalizeValue(argB(data)) @@ -55,23 +24,14 @@ function eqEvaluatorFactory( } } -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - export function eq( left: ComparisonOperand, right: ComparisonOperand, ): BasicExpression -export function eq( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive, +export function eq( + left: Aggregate, + right: unknown, ): BasicExpression -export function eq(left: Aggregate, right: any): BasicExpression -export function eq(left: any, right: any): BasicExpression { - return new Func( - `eq`, - [toExpression(left), toExpression(right)], - eqEvaluatorFactory, - ) +export function eq(left: unknown, right: unknown): BasicExpression { + return new Func(`eq`, [toExpression(left), toExpression(right)], eqFactory) } diff --git a/packages/db/src/query/builder/operators/factories.ts b/packages/db/src/query/builder/operators/factories.ts new file mode 100644 index 000000000..4c72d4565 --- /dev/null +++ b/packages/db/src/query/builder/operators/factories.ts @@ -0,0 +1,213 @@ +/** + * Factory generators for creating operator compile functions. + * + * These higher-order functions generate compile functions for common operator + * patterns, reducing duplication across operator implementations. + * + * All factories are fully typed - when used with defineOperator, the types flow + * through from the TArgs parameter to the callback function. + */ + +import type { CompiledExpression, EvaluatorFactory } from '../../ir.js' +import type { CompiledArgsFor, TypedEvaluatorFactory } from './define.js' + +// ============================================================ +// SHARED UTILITIES +// ============================================================ + +/** + * Check if a value is null or undefined (UNKNOWN in 3-valued logic). + * In SQL-like 3-valued logic, comparisons with UNKNOWN values return UNKNOWN. + */ +export function isUnknown(value: unknown): value is null | undefined { + return value === null || value === undefined +} + +// ============================================================ +// FACTORY GENERATORS +// ============================================================ + +/** + * Creates a typed evaluator factory for binary comparison operators (eq, gt, lt, gte, lte). + * Handles 3-valued logic automatically - returns null if either operand is null/undefined. + * + * @typeParam T - The type of values being compared (default: unknown) + * @param compare - The comparison function to apply to the two operands + * @returns A TypedEvaluatorFactory for binary comparison + * + * @example + * ```typescript + * // Untyped (works with any values) + * const eqFactory = comparison((a, b) => a === b) + * + * // Typed (a and b are numbers) + * const gtFactory = comparison((a, b) => a > b) + * + * // With defineOperator - type flows through from TArgs + * const gt = defineOperator({ + * name: 'gt', + * compile: comparison((a, b) => a > b) // a, b inferred as number + * }) + * ``` + */ +export function comparison( + compare: (a: T, b: T) => boolean, +): TypedEvaluatorFactory<[T, T]> { + const factory = ( + compiledArgs: CompiledArgsFor<[T, T]>, + ): CompiledExpression => { + const [argA, argB] = compiledArgs + + return (data: unknown) => { + const a = argA(data) + const b = argB(data) + if (isUnknown(a) || isUnknown(b)) return null + return compare(a, b) + } + } + return factory +} + +/** + * Creates an evaluator factory for variadic boolean operators (and, or). + * Implements proper 3-valued logic with short-circuit evaluation. + * + * @param config.shortCircuit - The value that causes early return (false for AND, true for OR) + * @param config.default - The result if no short-circuit occurs (true for AND, false for OR) + * @returns An EvaluatorFactory for boolean operations + * + * @example + * ```typescript + * const andFactory = booleanOp({ shortCircuit: false, default: true }) + * const orFactory = booleanOp({ shortCircuit: true, default: false }) + * ``` + */ +export function booleanOp(config: { + shortCircuit: boolean + default: boolean +}): EvaluatorFactory { + return (compiledArgs: Array): CompiledExpression => { + return (data: unknown) => { + let hasUnknown = false + for (const arg of compiledArgs) { + const result = arg(data) + if (result === config.shortCircuit) return config.shortCircuit + if (isUnknown(result)) hasUnknown = true + } + return hasUnknown ? null : config.default + } + } +} + +/** + * Creates a typed evaluator factory for unary transform operators. + * Applies a transformation function to a single argument. + * + * @typeParam T - The input type (default: unknown) + * @typeParam R - The output type (default: unknown) + * @param fn - The transformation function to apply + * @returns A TypedEvaluatorFactory for unary transforms + * + * @example + * ```typescript + * // Typed transform + * const doubleFactory = transform((v) => v * 2) + * + * // With defineOperator - type flows through from TArgs + * const double = defineOperator({ + * name: 'double', + * compile: transform((v) => v * 2) // v inferred as number + * }) + * + * // String transform + * const upper = defineOperator({ + * name: 'upper', + * compile: transform((v) => v.toUpperCase()) // v inferred as string + * }) + * ``` + */ +export function transform( + fn: (value: T) => R, +): TypedEvaluatorFactory<[T]> { + const factory = (compiledArgs: CompiledArgsFor<[T]>): CompiledExpression => { + const [arg] = compiledArgs + return (data: unknown) => fn(arg(data)) + } + return factory +} + +/** + * Creates a typed evaluator factory for binary numeric operators (add, subtract, multiply, divide). + * Applies a numeric operation to two operands, with a default value for null/undefined. + * + * @param operation - The numeric operation to apply + * @param defaultValue - The value to use when an operand is null/undefined (default: 0) + * @returns A TypedEvaluatorFactory for binary numeric operations + * + * @example + * ```typescript + * const addFactory = numeric((a, b) => a + b) + * const divideFactory = numeric((a, b) => b !== 0 ? a / b : null) + * + * // With defineOperator + * const modulo = defineOperator({ + * name: 'modulo', + * compile: numeric((a, b) => b !== 0 ? a % b : null) + * }) + * ``` + */ +export function numeric( + operation: (a: number, b: number) => number | null, + defaultValue: number = 0, +): TypedEvaluatorFactory<[number, number]> { + const factory = ( + compiledArgs: CompiledArgsFor<[number, number]>, + ): CompiledExpression => { + const [argA, argB] = compiledArgs + + return (data: unknown) => { + // Use type assertion because runtime values may be null/undefined despite type + const a = (argA(data) as number | null | undefined) ?? defaultValue + const b = (argB(data) as number | null | undefined) ?? defaultValue + return operation(a, b) + } + } + return factory +} + +/** + * Creates a typed evaluator factory for pattern matching operators (like, ilike). + * Handles 3-valued logic - returns null if either value or pattern is null/undefined. + * + * @param match - The matching function to apply (value, pattern) => boolean + * @returns A TypedEvaluatorFactory for pattern matching + * + * @example + * ```typescript + * const likeFactory = pattern((value, pattern) => evaluateLike(value, pattern, false)) + * const ilikeFactory = pattern((value, pattern) => evaluateLike(value, pattern, true)) + * + * // With defineOperator + * const startsWith = defineOperator({ + * name: 'startsWith', + * compile: pattern((value, prefix) => value.startsWith(prefix)) + * }) + * ``` + */ +export function pattern( + match: (value: string, pattern: string) => boolean, +): TypedEvaluatorFactory<[string, string]> { + const factory = ( + compiledArgs: CompiledArgsFor<[string, string]>, + ): CompiledExpression => { + const [valueArg, patternArg] = compiledArgs + + return (data: unknown) => { + const value = valueArg(data) + const patternVal = patternArg(data) + if (isUnknown(value) || isUnknown(patternVal)) return null + return match(value, patternVal) + } + } + return factory +} diff --git a/packages/db/src/query/builder/operators/gt.ts b/packages/db/src/query/builder/operators/gt.ts index 94238c811..821acf01a 100644 --- a/packages/db/src/query/builder/operators/gt.ts +++ b/packages/db/src/query/builder/operators/gt.ts @@ -1,76 +1,22 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { - Aggregate, - BasicExpression, - CompiledExpression, -} from '../../ir.js' -import type { RefProxy } from '../ref-proxy.js' -import type { RefLeaf } from '../types.js' +import { comparison } from './factories.js' +import type { Aggregate, BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ComparisonOperand } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -type ComparisonOperand = - | RefProxy - | RefLeaf - | T - | BasicExpression - | undefined - | null - -type ComparisonOperandPrimitive = - | T - | BasicExpression - | undefined - | null - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function gtEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - - return a > b - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// Factory using the comparison helper - cast to EvaluatorFactory for Func constructor +const gtFactory = /* #__PURE__*/ comparison( + (a, b) => a > b, +) as EvaluatorFactory export function gt( left: ComparisonOperand, right: ComparisonOperand, ): BasicExpression -export function gt( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive, +export function gt( + left: Aggregate, + right: unknown, ): BasicExpression -export function gt(left: Aggregate, right: any): BasicExpression -export function gt(left: any, right: any): BasicExpression { - return new Func( - `gt`, - [toExpression(left), toExpression(right)], - gtEvaluatorFactory, - ) +export function gt(left: unknown, right: unknown): BasicExpression { + return new Func(`gt`, [toExpression(left), toExpression(right)], gtFactory) } diff --git a/packages/db/src/query/builder/operators/gte.ts b/packages/db/src/query/builder/operators/gte.ts index f35ecc9f5..f75960e21 100644 --- a/packages/db/src/query/builder/operators/gte.ts +++ b/packages/db/src/query/builder/operators/gte.ts @@ -1,76 +1,22 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { - Aggregate, - BasicExpression, - CompiledExpression, -} from '../../ir.js' -import type { RefProxy } from '../ref-proxy.js' -import type { RefLeaf } from '../types.js' +import { comparison } from './factories.js' +import type { Aggregate, BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ComparisonOperand } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -type ComparisonOperand = - | RefProxy - | RefLeaf - | T - | BasicExpression - | undefined - | null - -type ComparisonOperandPrimitive = - | T - | BasicExpression - | undefined - | null - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function gteEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - - return a >= b - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// Factory using the comparison helper - cast to EvaluatorFactory for Func constructor +const gteFactory = /* #__PURE__*/ comparison( + (a, b) => a >= b, +) as EvaluatorFactory export function gte( left: ComparisonOperand, right: ComparisonOperand, ): BasicExpression -export function gte( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive, +export function gte( + left: Aggregate, + right: unknown, ): BasicExpression -export function gte(left: Aggregate, right: any): BasicExpression -export function gte(left: any, right: any): BasicExpression { - return new Func( - `gte`, - [toExpression(left), toExpression(right)], - gteEvaluatorFactory, - ) +export function gte(left: unknown, right: unknown): BasicExpression { + return new Func(`gte`, [toExpression(left), toExpression(right)], gteFactory) } diff --git a/packages/db/src/query/builder/operators/ilike.ts b/packages/db/src/query/builder/operators/ilike.ts index d880e822b..1a41c8e62 100644 --- a/packages/db/src/query/builder/operators/ilike.ts +++ b/packages/db/src/query/builder/operators/ilike.ts @@ -1,55 +1,26 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' +import { pattern } from './factories.js' import { evaluateLike } from './like.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import type { BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ExpressionLike, StringLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -type StringRef = - | BasicExpression - | BasicExpression - | BasicExpression -type StringLike = StringRef | string | null | undefined | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function ilikeEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const valueEvaluator = compiledArgs[0]! - const patternEvaluator = compiledArgs[1]! - - return (data: any) => { - const value = valueEvaluator(data) - const pattern = patternEvaluator(data) - // In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN - if (isUnknown(value) || isUnknown(pattern)) { - return null - } - return evaluateLike(value, pattern, true) - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const ilikeFactory = /* #__PURE__*/ pattern((value, patternStr) => + evaluateLike(value, patternStr, true), +) as EvaluatorFactory export function ilike( left: StringLike, right: StringLike, -): BasicExpression { +): BasicExpression +export function ilike( + left: ExpressionLike, + right: ExpressionLike, +): BasicExpression +export function ilike(left: unknown, right: unknown): BasicExpression { return new Func( `ilike`, [toExpression(left), toExpression(right)], - ilikeEvaluatorFactory, + ilikeFactory, ) } diff --git a/packages/db/src/query/builder/operators/in.ts b/packages/db/src/query/builder/operators/in.ts index 7027365d6..4b865a2a7 100644 --- a/packages/db/src/query/builder/operators/in.ts +++ b/packages/db/src/query/builder/operators/in.ts @@ -1,30 +1,15 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { isUnknown } from './factories.js' +import type { BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function inEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { +// IN requires a custom factory because it handles arrays specially +const inFactory: EvaluatorFactory = (compiledArgs) => { const valueEvaluator = compiledArgs[0]! const arrayEvaluator = compiledArgs[1]! - return (data: any) => { + return (data: unknown) => { const value = valueEvaluator(data) const array = arrayEvaluator(data) // In 3-valued logic, if the value is null/undefined, return UNKNOWN @@ -38,17 +23,9 @@ function inEvaluatorFactory( } } -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - export function inArray( value: ExpressionLike, array: ExpressionLike, ): BasicExpression { - return new Func( - `in`, - [toExpression(value), toExpression(array)], - inEvaluatorFactory, - ) + return new Func(`in`, [toExpression(value), toExpression(array)], inFactory) } diff --git a/packages/db/src/query/builder/operators/index.ts b/packages/db/src/query/builder/operators/index.ts index 6f463741e..80b625723 100644 --- a/packages/db/src/query/builder/operators/index.ts +++ b/packages/db/src/query/builder/operators/index.ts @@ -1,5 +1,5 @@ // Re-export all operators -// Importing from here will auto-register all evaluators +// Each operator is a function that creates Func IR nodes with embedded evaluators // Comparison operators export { eq } from './eq.js' @@ -36,3 +36,25 @@ export { divide } from './divide.js' // Null checking functions export { isNull } from './isNull.js' export { isUndefined } from './isUndefined.js' + +// Factory generators for custom operators +export { + isUnknown, + comparison, + booleanOp, + transform, + numeric, + pattern, +} from './factories.js' + +// Public API for defining custom operators +export { defineOperator, defineAggregate } from './define.js' +export type { + OperatorConfig, + AggregateDefinition, + ExpressionArg, + ExpressionArgs, + TypedCompiledExpression, + CompiledArgsFor, + TypedEvaluatorFactory, +} from './define.js' diff --git a/packages/db/src/query/builder/operators/isNull.ts b/packages/db/src/query/builder/operators/isNull.ts index a7860b5d1..4d57f166c 100644 --- a/packages/db/src/query/builder/operators/isNull.ts +++ b/packages/db/src/query/builder/operators/isNull.ts @@ -1,34 +1,7 @@ -import { Func } from '../../ir.js' -import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { defineOperator } from './define.js' +import { transform } from './factories.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isNullEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const arg = compiledArgs[0]! - - return (data: any) => { - const value = arg(data) - return value === null - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function isNull(value: ExpressionLike): BasicExpression { - return new Func(`isNull`, [toExpression(value)], isNullEvaluatorFactory) -} +export const isNull = /* #__PURE__*/ defineOperator({ + name: `isNull`, + compile: transform((v) => v === null), +}) diff --git a/packages/db/src/query/builder/operators/isUndefined.ts b/packages/db/src/query/builder/operators/isUndefined.ts index 743bb84bd..879c558e0 100644 --- a/packages/db/src/query/builder/operators/isUndefined.ts +++ b/packages/db/src/query/builder/operators/isUndefined.ts @@ -1,38 +1,10 @@ -import { Func } from '../../ir.js' -import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUndefinedEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const arg = compiledArgs[0]! - - return (data: any) => { - const value = arg(data) - return value === undefined - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function isUndefined(value: ExpressionLike): BasicExpression { - return new Func( - `isUndefined`, - [toExpression(value)], - isUndefinedEvaluatorFactory, - ) -} +import { defineOperator } from './define.js' +import { transform } from './factories.js' + +export const isUndefined = /* #__PURE__*/ defineOperator< + boolean, + [value: unknown] +>({ + name: `isUndefined`, + compile: transform((v) => v === undefined), +}) diff --git a/packages/db/src/query/builder/operators/length.ts b/packages/db/src/query/builder/operators/length.ts index 1b4188d66..2e6f484fb 100644 --- a/packages/db/src/query/builder/operators/length.ts +++ b/packages/db/src/query/builder/operators/length.ts @@ -1,33 +1,14 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { CompiledExpression } from '../../ir.js' +import { transform } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' import type { ExpressionLike, NumericFunctionReturnType } from './types.js' -// ============================================================ -// EVALUATOR -// ============================================================ - -function lengthEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const arg = compiledArgs[0]! - - return (data: any) => { - const value = arg(data) - if (typeof value === `string`) { - return value.length - } - if (Array.isArray(value)) { - return value.length - } - return 0 - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const lengthFactory = /* #__PURE__*/ transform((v) => { + if (typeof v === `string`) return v.length + if (Array.isArray(v)) return v.length + return 0 +}) as EvaluatorFactory export function length( arg: T, @@ -35,6 +16,6 @@ export function length( return new Func( `length`, [toExpression(arg)], - lengthEvaluatorFactory, + lengthFactory, ) as NumericFunctionReturnType } diff --git a/packages/db/src/query/builder/operators/like.ts b/packages/db/src/query/builder/operators/like.ts index eac0ca042..3e3480807 100644 --- a/packages/db/src/query/builder/operators/like.ts +++ b/packages/db/src/query/builder/operators/like.ts @@ -1,39 +1,24 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' - -// ============================================================ -// TYPES -// ============================================================ - -type StringRef = - | BasicExpression - | BasicExpression - | BasicExpression -type StringLike = StringRef | string | null | undefined | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} +import { pattern } from './factories.js' +import type { BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ExpressionLike, StringLike } from './types.js' /** - * Evaluates LIKE patterns + * Evaluates SQL LIKE patterns. + * Exported for use by ilike. */ -function evaluateLike( - value: any, - pattern: any, - caseInsensitive: boolean, +export function evaluateLike( + value: unknown, + patternStr: unknown, + caseInsensitive: boolean = false, ): boolean { - if (typeof value !== `string` || typeof pattern !== `string`) { + if (typeof value !== `string` || typeof patternStr !== `string`) { return false } const searchValue = caseInsensitive ? value.toLowerCase() : value - const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern + const searchPattern = caseInsensitive ? patternStr.toLowerCase() : patternStr // Convert SQL LIKE pattern to regex // First escape all regex special chars except % and _ @@ -47,39 +32,22 @@ function evaluateLike( return regex.test(searchValue) } -function likeEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const valueEvaluator = compiledArgs[0]! - const patternEvaluator = compiledArgs[1]! - - return (data: any) => { - const value = valueEvaluator(data) - const pattern = patternEvaluator(data) - // In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN - if (isUnknown(value) || isUnknown(pattern)) { - return null - } - return evaluateLike(value, pattern, false) - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const likeFactory = /* #__PURE__*/ pattern((value, patternStr) => + evaluateLike(value, patternStr, false), +) as EvaluatorFactory export function like( left: StringLike, right: StringLike, ): BasicExpression -export function like(left: any, right: any): BasicExpression { +export function like( + left: ExpressionLike, + right: ExpressionLike, +): BasicExpression +export function like(left: unknown, right: unknown): BasicExpression { return new Func( `like`, [toExpression(left), toExpression(right)], - likeEvaluatorFactory, + likeFactory, ) } - -// Export for use by ilike -export { evaluateLike } diff --git a/packages/db/src/query/builder/operators/lower.ts b/packages/db/src/query/builder/operators/lower.ts index 1cffb3683..057885926 100644 --- a/packages/db/src/query/builder/operators/lower.ts +++ b/packages/db/src/query/builder/operators/lower.ts @@ -1,27 +1,12 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { CompiledExpression } from '../../ir.js' +import { transform } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' import type { ExpressionLike, StringFunctionReturnType } from './types.js' -// ============================================================ -// EVALUATOR -// ============================================================ - -function lowerEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const arg = compiledArgs[0]! - - return (data: any) => { - const value = arg(data) - return typeof value === `string` ? value.toLowerCase() : value - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const lowerFactory = /* #__PURE__*/ transform((v) => + typeof v === `string` ? v.toLowerCase() : v, +) as EvaluatorFactory export function lower( arg: T, @@ -29,6 +14,6 @@ export function lower( return new Func( `lower`, [toExpression(arg)], - lowerEvaluatorFactory, + lowerFactory, ) as StringFunctionReturnType } diff --git a/packages/db/src/query/builder/operators/lt.ts b/packages/db/src/query/builder/operators/lt.ts index 89c31bd99..249dffbcc 100644 --- a/packages/db/src/query/builder/operators/lt.ts +++ b/packages/db/src/query/builder/operators/lt.ts @@ -1,76 +1,22 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { - Aggregate, - BasicExpression, - CompiledExpression, -} from '../../ir.js' -import type { RefProxy } from '../ref-proxy.js' -import type { RefLeaf } from '../types.js' +import { comparison } from './factories.js' +import type { Aggregate, BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ComparisonOperand } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -type ComparisonOperand = - | RefProxy - | RefLeaf - | T - | BasicExpression - | undefined - | null - -type ComparisonOperandPrimitive = - | T - | BasicExpression - | undefined - | null - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function ltEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - - return a < b - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// Factory using the comparison helper - cast to EvaluatorFactory for Func constructor +const ltFactory = /* #__PURE__*/ comparison( + (a, b) => a < b, +) as EvaluatorFactory export function lt( left: ComparisonOperand, right: ComparisonOperand, ): BasicExpression -export function lt( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive, +export function lt( + left: Aggregate, + right: unknown, ): BasicExpression -export function lt(left: Aggregate, right: any): BasicExpression -export function lt(left: any, right: any): BasicExpression { - return new Func( - `lt`, - [toExpression(left), toExpression(right)], - ltEvaluatorFactory, - ) +export function lt(left: unknown, right: unknown): BasicExpression { + return new Func(`lt`, [toExpression(left), toExpression(right)], ltFactory) } diff --git a/packages/db/src/query/builder/operators/lte.ts b/packages/db/src/query/builder/operators/lte.ts index 1ba792d74..5b194e412 100644 --- a/packages/db/src/query/builder/operators/lte.ts +++ b/packages/db/src/query/builder/operators/lte.ts @@ -1,76 +1,22 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { - Aggregate, - BasicExpression, - CompiledExpression, -} from '../../ir.js' -import type { RefProxy } from '../ref-proxy.js' -import type { RefLeaf } from '../types.js' +import { comparison } from './factories.js' +import type { Aggregate, BasicExpression, EvaluatorFactory } from '../../ir.js' +import type { ComparisonOperand } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -type ComparisonOperand = - | RefProxy - | RefLeaf - | T - | BasicExpression - | undefined - | null - -type ComparisonOperandPrimitive = - | T - | BasicExpression - | undefined - | null - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function lteEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - - return a <= b - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// Factory using the comparison helper - cast to EvaluatorFactory for Func constructor +const lteFactory = /* #__PURE__*/ comparison( + (a, b) => a <= b, +) as EvaluatorFactory export function lte( left: ComparisonOperand, right: ComparisonOperand, ): BasicExpression -export function lte( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive, +export function lte( + left: Aggregate, + right: unknown, ): BasicExpression -export function lte(left: Aggregate, right: any): BasicExpression -export function lte(left: any, right: any): BasicExpression { - return new Func( - `lte`, - [toExpression(left), toExpression(right)], - lteEvaluatorFactory, - ) +export function lte(left: unknown, right: unknown): BasicExpression { + return new Func(`lte`, [toExpression(left), toExpression(right)], lteFactory) } diff --git a/packages/db/src/query/builder/operators/multiply.ts b/packages/db/src/query/builder/operators/multiply.ts index 95e328474..f159e8ee4 100644 --- a/packages/db/src/query/builder/operators/multiply.ts +++ b/packages/db/src/query/builder/operators/multiply.ts @@ -1,40 +1,12 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { numeric } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' +import type { BinaryNumericReturnType, ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for binary numeric operations (combines nullability of both operands) -type BinaryNumericReturnType<_T1, _T2> = BasicExpression< - number | undefined | null -> - -// ============================================================ -// EVALUATOR -// ============================================================ - -function multiplyEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - return (a ?? 0) * (b ?? 0) - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const multiplyFactory = /* #__PURE__*/ numeric( + (a, b) => a * b, +) as EvaluatorFactory export function multiply( left: T1, @@ -43,6 +15,6 @@ export function multiply( return new Func( `multiply`, [toExpression(left), toExpression(right)], - multiplyEvaluatorFactory, + multiplyFactory, ) as BinaryNumericReturnType } diff --git a/packages/db/src/query/builder/operators/not.ts b/packages/db/src/query/builder/operators/not.ts index 9a59f9461..c0d5627cf 100644 --- a/packages/db/src/query/builder/operators/not.ts +++ b/packages/db/src/query/builder/operators/not.ts @@ -1,45 +1,10 @@ -import { Func } from '../../ir.js' -import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function notEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const arg = compiledArgs[0]! - - return (data: any) => { - // 3-valued logic for NOT: - // - NOT null = null - // - NOT true = false - // - NOT false = true - const result = arg(data) - if (isUnknown(result)) { - return null - } - return !result - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function not(value: ExpressionLike): BasicExpression { - return new Func(`not`, [toExpression(value)], notEvaluatorFactory) -} +import { defineOperator } from './define.js' +import { isUnknown, transform } from './factories.js' + +// NOT: returns null for unknown, negates boolean values +// Note: Runtime returns null for unknown values (3-valued logic), +// but typed as boolean for backward compatibility +export const not = /* #__PURE__*/ defineOperator({ + name: `not`, + compile: transform((v) => (isUnknown(v) ? null : !v)), +}) diff --git a/packages/db/src/query/builder/operators/or.ts b/packages/db/src/query/builder/operators/or.ts index e7dd4f7b1..ef188b59c 100644 --- a/packages/db/src/query/builder/operators/or.ts +++ b/packages/db/src/query/builder/operators/or.ts @@ -1,54 +1,14 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { booleanOp } from './factories.js' +import type { BasicExpression } from '../../ir.js' +import type { ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// ============================================================ -// EVALUATOR -// ============================================================ - -function isUnknown(value: any): boolean { - return value === null || value === undefined -} - -function orEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - return (data: any) => { - // 3-valued logic for OR: - // - true OR anything = true (short-circuit) - // - null OR anything (except true) = null - // - false OR false = false - let hasUnknown = false - for (const compiledArg of compiledArgs) { - const result = compiledArg(data) - if (result === true) { - return true - } - if (isUnknown(result)) { - hasUnknown = true - } - } - // If we got here, no operand was true - // If any operand was null, return null (UNKNOWN) - if (hasUnknown) { - return null - } - - return false - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +// OR: short-circuits on true, returns false if all are false +const orFactory = /* #__PURE__*/ booleanOp({ + shortCircuit: true, + default: false, +}) // Overloads for or() - support 2 or more arguments, or an array export function or( @@ -71,7 +31,7 @@ export function or( return new Func( `or`, leftOrArgs.map((arg) => toExpression(arg)), - orEvaluatorFactory, + orFactory, ) } // Handle variadic overload @@ -79,6 +39,6 @@ export function or( return new Func( `or`, allArgs.map((arg) => toExpression(arg)), - orEvaluatorFactory, + orFactory, ) } diff --git a/packages/db/src/query/builder/operators/subtract.ts b/packages/db/src/query/builder/operators/subtract.ts index ec15e8b7c..9161c4afb 100644 --- a/packages/db/src/query/builder/operators/subtract.ts +++ b/packages/db/src/query/builder/operators/subtract.ts @@ -1,40 +1,12 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { BasicExpression, CompiledExpression } from '../../ir.js' +import { numeric } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' +import type { BinaryNumericReturnType, ExpressionLike } from './types.js' -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for binary numeric operations (combines nullability of both operands) -type BinaryNumericReturnType<_T1, _T2> = BasicExpression< - number | undefined | null -> - -// ============================================================ -// EVALUATOR -// ============================================================ - -function subtractEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - - return (data: any) => { - const a = argA(data) - const b = argB(data) - return (a ?? 0) - (b ?? 0) - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const subtractFactory = /* #__PURE__*/ numeric( + (a, b) => a - b, +) as EvaluatorFactory export function subtract( left: T1, @@ -43,6 +15,6 @@ export function subtract( return new Func( `subtract`, [toExpression(left), toExpression(right)], - subtractEvaluatorFactory, + subtractFactory, ) as BinaryNumericReturnType } diff --git a/packages/db/src/query/builder/operators/upper.ts b/packages/db/src/query/builder/operators/upper.ts index e72e4d470..f032f364d 100644 --- a/packages/db/src/query/builder/operators/upper.ts +++ b/packages/db/src/query/builder/operators/upper.ts @@ -1,27 +1,12 @@ import { Func } from '../../ir.js' import { toExpression } from '../ref-proxy.js' -import type { CompiledExpression } from '../../ir.js' +import { transform } from './factories.js' +import type { EvaluatorFactory } from '../../ir.js' import type { ExpressionLike, StringFunctionReturnType } from './types.js' -// ============================================================ -// EVALUATOR -// ============================================================ - -function upperEvaluatorFactory( - compiledArgs: Array, - _isSingleRow: boolean, -): CompiledExpression { - const arg = compiledArgs[0]! - - return (data: any) => { - const value = arg(data) - return typeof value === `string` ? value.toUpperCase() : value - } -} - -// ============================================================ -// BUILDER FUNCTION -// ============================================================ +const upperFactory = /* #__PURE__*/ transform((v) => + typeof v === `string` ? v.toUpperCase() : v, +) as EvaluatorFactory export function upper( arg: T, @@ -29,6 +14,6 @@ export function upper( return new Func( `upper`, [toExpression(arg)], - upperEvaluatorFactory, + upperFactory, ) as StringFunctionReturnType } diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 99cb408c4..eeae41d54 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -12,7 +12,7 @@ export { type InferResultType, } from './builder/index.js' -// Expression functions exports - now from operator modules for tree-shaking +// Expression functions exports - operators and aggregates export { // Comparison operators eq, @@ -42,9 +42,26 @@ export { subtract, multiply, divide, + // Factory generators for custom operators + isUnknown, + comparison, + booleanOp, + transform, + numeric, + pattern, + // Public API for defining custom operators and aggregates + defineOperator, + defineAggregate, + type OperatorConfig, + type AggregateDefinition, + type ExpressionArg, + type ExpressionArgs, + type TypedCompiledExpression, + type CompiledArgsFor, + type TypedEvaluatorFactory, } from './builder/operators/index.js' -// Aggregates - now from aggregate modules for tree-shaking +// Aggregates export { count, avg, @@ -57,8 +74,6 @@ export { } from './builder/aggregates/index.js' // Types for custom operators and aggregates -// Custom operators: create a Func with your own factory as the 3rd argument -// Custom aggregates: create an Aggregate with your own config as the 3rd argument export { Func, Aggregate, diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index 95c2c13ce..74c76cec0 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -26,7 +26,7 @@ import { upper, } from '../../../src/query/builder/functions.js' import type { RefLeaf } from '../../../src/query/builder/types.js' -import type { Aggregate, BasicExpression } from '../../../src/query/ir.js' +import type { Aggregate, BasicExpression, Func } from '../../../src/query/ir.js' // Sample data types for comprehensive callback type testing type User = { @@ -243,9 +243,7 @@ describe(`Query Builder Callback Types`, () => { expectTypeOf( or(eq(user.active, false), lt(user.age, 18)), ).toEqualTypeOf>() - expectTypeOf(not(eq(user.active, false))).toEqualTypeOf< - BasicExpression - >() + expectTypeOf(not(eq(user.active, false))).toEqualTypeOf>() return and( eq(user.active, true), diff --git a/packages/db/tests/query/compiler/custom-aggregates.test-d.ts b/packages/db/tests/query/compiler/custom-aggregates.test-d.ts new file mode 100644 index 000000000..1713dd3a6 --- /dev/null +++ b/packages/db/tests/query/compiler/custom-aggregates.test-d.ts @@ -0,0 +1,615 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { createCollection } from '../../../src/collection/index.js' +import { + Aggregate, + avg, + count, + createLiveQueryCollection, + defineAggregate, + sum, +} from '../../../src/query/index.js' +import { PropRef, Value } from '../../../src/query/ir.js' +import { toExpression } from '../../../src/query/builder/ref-proxy.js' +import { mockSyncCollectionOptions } from '../../utils.js' +import type { + AggregateConfig, + AggregateFactory, + ExpressionArg, + ValueExtractor, +} from '../../../src/query/index.js' +import type { RefLeaf } from '../../../src/query/builder/types.js' + +// Sample data type for tests +type TestItem = { + id: number + category: string + price: number + quantity: number +} + +const sampleItems: Array = [ + { id: 1, category: `A`, price: 10, quantity: 2 }, + { id: 2, category: `A`, price: 20, quantity: 3 }, + { id: 3, category: `B`, price: 15, quantity: 1 }, +] + +function createTestCollection() { + return createCollection( + mockSyncCollectionOptions({ + id: `test-custom-aggregates-types`, + getKey: (item) => item.id, + initialData: sampleItems, + }), + ) +} + +describe(`Custom Aggregate Types`, () => { + describe(`defineAggregate return types`, () => { + test(`defineAggregate returns a function that produces Aggregate`, () => { + const product = defineAggregate({ + name: `product`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => { + let result = 1 + for (const [value, multiplicity] of values) { + for (let i = 0; i < multiplicity; i++) { + result *= value + } + } + return result + }, + }), + valueTransform: `numeric`, + }) + + // The returned function should accept unknown arg and return Aggregate + expectTypeOf(product).toBeFunction() + expectTypeOf(product).returns.toEqualTypeOf>() + }) + + test(`defineAggregate with nullable return type`, () => { + const median = defineAggregate({ + name: `median`, + factory: (valueExtractor) => ({ + preMap: (data: any) => [valueExtractor(data)], + reduce: (values: Array<[Array, number]>) => { + const allValues: Array = [] + for (const [valueArray, multiplicity] of values) { + for (const value of valueArray) { + for (let i = 0; i < multiplicity; i++) { + allValues.push(value) + } + } + } + return allValues + }, + postMap: (allValues: Array) => { + if (allValues.length === 0) return null + const sorted = [...allValues].sort((a, b) => a - b) + return sorted[Math.floor(sorted.length / 2)]! + }, + }), + valueTransform: `raw`, + }) + + expectTypeOf(median).returns.toEqualTypeOf>() + }) + + test(`defineAggregate with array return type`, () => { + const collect = defineAggregate>({ + name: `collect`, + factory: (valueExtractor) => ({ + preMap: (data: any) => [valueExtractor(data)], + reduce: (values: Array<[Array, number]>) => { + const allValues: Array = [] + for (const [valueArray, multiplicity] of values) { + for (const value of valueArray) { + for (let i = 0; i < multiplicity; i++) { + allValues.push(value) + } + } + } + return allValues + }, + }), + valueTransform: `raw`, + }) + + expectTypeOf(collect).returns.toEqualTypeOf>>() + }) + + test(`defineAggregate without generic defaults to unknown`, () => { + const generic = defineAggregate({ + name: `generic`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[unknown, number]>) => values[0]?.[0], + }), + valueTransform: `raw`, + }) + + expectTypeOf(generic).returns.toEqualTypeOf>() + }) + }) + + describe(`defineAggregate argument types`, () => { + test(`typed aggregate with TArg generic`, () => { + // Define with explicit argument type + const typedSum = defineAggregate({ + name: `sum`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => + values.reduce((acc, [v, m]) => acc + v * m, 0), + }), + valueTransform: `numeric`, + }) + + // Should accept literal number + const result = typedSum(10) + expectTypeOf(result).toEqualTypeOf>() + + // Should also accept RefLeaf + const refResult = typedSum({} as RefLeaf) + expectTypeOf(refResult).toEqualTypeOf>() + }) + + test(`typed aggregate with string argument type`, () => { + const concatAgg = defineAggregate({ + name: `concat`, + factory: (valueExtractor) => ({ + preMap: (data: any) => String(valueExtractor(data)), + reduce: (values: Array<[string, number]>) => + values.map(([v]) => v).join(`,`), + }), + valueTransform: `raw`, + }) + + // Should accept string literal + const result = concatAgg(`hello`) + expectTypeOf(result).toEqualTypeOf>() + + // Should also accept RefLeaf + const refResult = concatAgg({} as RefLeaf) + expectTypeOf(refResult).toEqualTypeOf>() + }) + + test(`ExpressionArg type allows value or expression for aggregates`, () => { + // ExpressionArg should accept: + type StrArg = ExpressionArg + + // - literal string + expectTypeOf().toMatchTypeOf() + // - RefLeaf + expectTypeOf>().toMatchTypeOf() + }) + + test(`aggregate accepts literal number argument`, () => { + const sumAgg = defineAggregate({ + name: `sum`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => + values.reduce((acc, [v, m]) => acc + v * m, 0), + }), + valueTransform: `numeric`, + }) + + // Should accept literal number + const result = sumAgg(10) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`aggregate accepts Value IR node as argument`, () => { + const product = defineAggregate({ + name: `product`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: () => 1, + }), + valueTransform: `numeric`, + }) + + // Should accept Value node + const result = product(new Value(10)) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`aggregate accepts PropRef IR node as argument`, () => { + const sumAgg = defineAggregate({ + name: `sum`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => + values.reduce((acc, [v, m]) => acc + v * m, 0), + }), + valueTransform: `numeric`, + }) + + // Should accept PropRef node + const result = sumAgg(new PropRef([`items`, `price`])) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`aggregate accepts string literal argument`, () => { + const concatAgg = defineAggregate({ + name: `concat`, + factory: (valueExtractor) => ({ + preMap: (data: any) => String(valueExtractor(data)), + reduce: (values: Array<[string, number]>) => + values.map(([v]) => v).join(`,`), + }), + valueTransform: `raw`, + }) + + // Should accept string literal + const result = concatAgg(`hello`) + expectTypeOf(result).toEqualTypeOf>() + }) + }) + + describe(`defineAggregate factory callback types`, () => { + test(`factory callback receives ValueExtractor`, () => { + defineAggregate({ + name: `test`, + factory: (valueExtractor) => { + // valueExtractor should be ValueExtractor type + expectTypeOf(valueExtractor).toMatchTypeOf() + + return { + preMap: valueExtractor, + reduce: () => 0, + } + }, + valueTransform: `numeric`, + }) + }) + + test(`factory callback returns object with preMap and reduce`, () => { + defineAggregate({ + name: `test`, + factory: (valueExtractor) => { + const result = { + preMap: valueExtractor, + reduce: (_values: Array<[number, number]>) => 0, + } + + // Should have preMap and reduce + expectTypeOf(result.preMap).toMatchTypeOf() + expectTypeOf(result.reduce).toBeFunction() + + return result + }, + valueTransform: `numeric`, + }) + }) + + test(`factory callback can return optional postMap`, () => { + defineAggregate({ + name: `test`, + factory: (valueExtractor) => ({ + preMap: (data: any) => { + const value = valueExtractor(data) + return { sum: value, n: 1 } + }, + reduce: (_values: Array<[{ sum: number; n: number }, number]>) => ({ + sum: 0, + n: 0, + }), + postMap: (acc: { sum: number; n: number }) => { + expectTypeOf(acc).toEqualTypeOf<{ sum: number; n: number }>() + return acc.n > 0 ? acc.sum / acc.n : 0 + }, + }), + valueTransform: `raw`, + }) + }) + + test(`AggregateFactory type is correctly typed`, () => { + const factory: AggregateFactory = (valueExtractor) => { + expectTypeOf(valueExtractor).toMatchTypeOf() + return { + preMap: valueExtractor, + reduce: () => 0, + } + } + + expectTypeOf(factory).toMatchTypeOf() + }) + }) + + describe(`custom aggregates in queries`, () => { + const testCollection = createTestCollection() + + test(`custom aggregate accepts ref proxy in query context`, () => { + const product = defineAggregate({ + name: `product`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => { + let result = 1 + for (const [value, multiplicity] of values) { + for (let i = 0; i < multiplicity; i++) { + result *= value + } + } + return result + }, + }), + valueTransform: `numeric`, + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + .groupBy(({ items }) => items.category) + // items.price is RefLeaf + .select(({ items }) => ({ + category: items.category, + priceProduct: product(items.price), + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + category: string + priceProduct: number + }> + >() + + const item = result.get(`A`) + expectTypeOf(item).toEqualTypeOf< + | { + category: string + priceProduct: number + } + | undefined + >() + }) + + test(`custom aggregate with different ref proxy field`, () => { + const sumAgg = defineAggregate({ + name: `customSum`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => + values.reduce((acc, [v, m]) => acc + v * m, 0), + }), + valueTransform: `numeric`, + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + .groupBy(({ items }) => items.category) + // items.quantity is RefLeaf + .select(({ items }) => ({ + category: items.category, + totalQty: sumAgg(items.quantity), + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + category: string + totalQty: number + }> + >() + }) + + test(`custom aggregate alongside built-in aggregates`, () => { + const variance = defineAggregate({ + name: `variance`, + factory: (valueExtractor: ValueExtractor) => ({ + preMap: (data: any) => { + const value = valueExtractor(data) + return { sum: value, sumSq: value * value, n: 1 } + }, + reduce: ( + values: Array<[{ sum: number; sumSq: number; n: number }, number]>, + ) => { + let totalSum = 0 + let totalSumSq = 0 + let totalN = 0 + for (const [{ sum: s, sumSq, n }, multiplicity] of values) { + totalSum += s * multiplicity + totalSumSq += sumSq * multiplicity + totalN += n * multiplicity + } + return { sum: totalSum, sumSq: totalSumSq, n: totalN } + }, + postMap: (acc: { sum: number; sumSq: number; n: number }) => { + if (acc.n === 0) return 0 + const mean = acc.sum / acc.n + return acc.sumSq / acc.n - mean * mean + }, + }), + valueTransform: `raw`, + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + // Built-in aggregates + totalPrice: sum(items.price), + avgPrice: avg(items.price), + itemCount: count(items.id), + // Custom aggregate + priceVariance: variance(items.price), + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + category: string + totalPrice: number + avgPrice: number + itemCount: number + priceVariance: number + }> + >() + }) + + test(`multiple custom aggregates in same query`, () => { + const product = defineAggregate({ + name: `product`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: () => 1, + }), + valueTransform: `numeric`, + }) + + const range = defineAggregate({ + name: `range`, + factory: (valueExtractor) => ({ + preMap: (data: any) => { + const v = valueExtractor(data) + return { min: v, max: v } + }, + reduce: () => ({ min: 0, max: 0 }), + postMap: (acc: { min: number; max: number }) => acc.max - acc.min, + }), + valueTransform: `raw`, + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + priceProduct: product(items.price), + priceRange: range(items.price), + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + category: string + priceProduct: number + priceRange: number + }> + >() + }) + }) + + describe(`Aggregate IR node types`, () => { + test(`Aggregate type parameter affects node type`, () => { + const numberAgg = new Aggregate(`sum`, []) + expectTypeOf(numberAgg).toMatchTypeOf>() + + const arrayAgg = new Aggregate>(`collect`, []) + expectTypeOf(arrayAgg).toMatchTypeOf>>() + + const nullableAgg = new Aggregate(`median`, []) + expectTypeOf(nullableAgg).toMatchTypeOf>() + }) + + test(`Aggregate with config is still properly typed`, () => { + const config: AggregateConfig = { + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => + values.reduce((acc, [v, m]) => acc + v * m, 0), + }), + valueTransform: `numeric`, + } + + const agg = new Aggregate(`custom`, [toExpression(10)], config) + + expectTypeOf(agg).toMatchTypeOf>() + expectTypeOf(agg.name).toEqualTypeOf() + expectTypeOf(agg.type).toEqualTypeOf<`agg`>() + expectTypeOf(agg.config).toEqualTypeOf() + }) + + test(`Aggregate args array accepts BasicExpression types`, () => { + const agg = new Aggregate(`test`, [ + new Value(10), + new PropRef([`items`, `price`]), + ]) + + expectTypeOf(agg).toMatchTypeOf>() + expectTypeOf(agg.args).toBeArray() + }) + + test(`manual Aggregate creation with typed config`, () => { + const productConfig: AggregateConfig = { + factory: (valueExtractor: ValueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => { + let result = 1 + for (const [value, multiplicity] of values) { + for (let i = 0; i < multiplicity; i++) { + result *= value + } + } + return result + }, + }), + valueTransform: `numeric`, + } + + function product(arg: T): Aggregate { + return new Aggregate(`product`, [toExpression(arg)], productConfig) + } + + expectTypeOf(product).returns.toEqualTypeOf>() + + const agg = product(10) + expectTypeOf(agg).toMatchTypeOf>() + }) + }) + + describe(`AggregateConfig type`, () => { + test(`AggregateConfig valueTransform literal types`, () => { + const numericConfig: AggregateConfig = { + factory: (ve) => ({ preMap: ve, reduce: () => 0 }), + valueTransform: `numeric`, + } + expectTypeOf(numericConfig.valueTransform).toEqualTypeOf< + `numeric` | `numericOrDate` | `raw` + >() + + const rawConfig: AggregateConfig = { + factory: (ve) => ({ preMap: ve, reduce: () => 0 }), + valueTransform: `raw`, + } + expectTypeOf(rawConfig.valueTransform).toEqualTypeOf< + `numeric` | `numericOrDate` | `raw` + >() + + const dateConfig: AggregateConfig = { + factory: (ve) => ({ preMap: ve, reduce: () => 0 }), + valueTransform: `numericOrDate`, + } + expectTypeOf(dateConfig.valueTransform).toEqualTypeOf< + `numeric` | `numericOrDate` | `raw` + >() + }) + + test(`AggregateConfig factory type`, () => { + const config: AggregateConfig = { + factory: (valueExtractor) => { + expectTypeOf(valueExtractor).toMatchTypeOf() + return { + preMap: valueExtractor, + reduce: () => 0, + } + }, + valueTransform: `numeric`, + } + + expectTypeOf(config.factory).toMatchTypeOf() + }) + }) +}) diff --git a/packages/db/tests/query/compiler/custom-aggregates.test.ts b/packages/db/tests/query/compiler/custom-aggregates.test.ts index d81308b29..e8caf526a 100644 --- a/packages/db/tests/query/compiler/custom-aggregates.test.ts +++ b/packages/db/tests/query/compiler/custom-aggregates.test.ts @@ -5,6 +5,7 @@ import { avg, count, createLiveQueryCollection, + defineAggregate, sum, } from '../../../src/query/index.js' import { toExpression } from '../../../src/query/builder/ref-proxy.js' @@ -217,3 +218,211 @@ describe(`Custom Aggregates`, () => { }) }) }) + +// ============================================================ +// defineAggregate public API tests +// ============================================================ + +describe(`defineAggregate public API`, () => { + describe(`typed custom aggregates`, () => { + it(`product aggregate with type annotation`, () => { + const typedProduct = defineAggregate({ + name: `product`, + factory: (valueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => { + let result = 1 + for (const [value, multiplicity] of values) { + for (let i = 0; i < multiplicity; i++) { + result *= value + } + } + return result + }, + }), + valueTransform: `numeric`, + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + priceProduct: typedProduct(items.price), + })), + }) + + expect(result.size).toBe(2) + // Category A: 10 * 20 = 200 + expect(result.get(`A`)?.priceProduct).toBe(200) + // Category B: 15 * 25 = 375 + expect(result.get(`B`)?.priceProduct).toBe(375) + }) + + it(`range aggregate (max - min) with postMap`, () => { + const range = defineAggregate({ + name: `range`, + factory: (valueExtractor) => ({ + preMap: (data: any) => { + const value = valueExtractor(data) + return { min: value, max: value } + }, + reduce: (values: Array<[{ min: number; max: number }, number]>) => { + let min = Infinity + let max = -Infinity + for (const [{ min: vMin, max: vMax }, multiplicity] of values) { + if (multiplicity > 0) { + if (vMin < min) min = vMin + if (vMax > max) max = vMax + } + } + return { min, max } + }, + postMap: (acc: { min: number; max: number }) => acc.max - acc.min, + }), + valueTransform: `raw`, + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + priceRange: range(items.price), + })), + }) + + expect(result.size).toBe(2) + // Category A: max(10, 20) - min(10, 20) = 20 - 10 = 10 + expect(result.get(`A`)?.priceRange).toBe(10) + // Category B: max(15, 25) - min(15, 25) = 25 - 15 = 10 + expect(result.get(`B`)?.priceRange).toBe(10) + }) + + it(`variance aggregate with complex reduce logic`, () => { + const typedVariance = defineAggregate({ + name: `variance`, + factory: (valueExtractor: ValueExtractor) => ({ + preMap: (data: any) => { + const value = valueExtractor(data) + return { sum: value, sumSq: value * value, n: 1 } + }, + reduce: ( + values: Array<[{ sum: number; sumSq: number; n: number }, number]>, + ) => { + let totalSum = 0 + let totalSumSq = 0 + let totalN = 0 + for (const [{ sum: s, sumSq, n }, multiplicity] of values) { + totalSum += s * multiplicity + totalSumSq += sumSq * multiplicity + totalN += n * multiplicity + } + return { sum: totalSum, sumSq: totalSumSq, n: totalN } + }, + postMap: (acc: { sum: number; sumSq: number; n: number }) => { + if (acc.n === 0) return 0 + const mean = acc.sum / acc.n + return acc.sumSq / acc.n - mean * mean + }, + }), + valueTransform: `raw`, + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + priceVariance: typedVariance(items.price), + })), + }) + + expect(result.size).toBe(2) + + // Category A: prices 10, 20 -> mean = 15, variance = ((10-15)² + (20-15)²) / 2 = 25 + expect(result.get(`A`)?.priceVariance).toBe(25) + + // Category B: prices 15, 25 -> mean = 20, variance = ((15-20)² + (25-20)²) / 2 = 25 + expect(result.get(`B`)?.priceVariance).toBe(25) + }) + }) + + describe(`defineAggregate works with built-in aggregates`, () => { + it(`custom aggregate alongside built-in aggregates`, () => { + const median = defineAggregate({ + name: `median`, + factory: (valueExtractor) => ({ + preMap: (data: any) => [valueExtractor(data)], + reduce: (values: Array<[Array, number]>) => { + const allValues: Array = [] + for (const [valueArray, multiplicity] of values) { + for (const value of valueArray) { + for (let i = 0; i < multiplicity; i++) { + allValues.push(value) + } + } + } + return allValues + }, + postMap: (allValues: Array) => { + if (allValues.length === 0) return null + const sorted = [...allValues].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + if (sorted.length % 2 === 0) { + return (sorted[mid - 1]! + sorted[mid]!) / 2 + } + return sorted[mid]! + }, + }), + valueTransform: `raw`, + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + avgPrice: avg(items.price), + sumPrice: sum(items.price), + itemCount: count(items.id), + medianPrice: median(items.price), + })), + }) + + expect(result.size).toBe(2) + + const categoryA = result.get(`A`) + expect(categoryA?.avgPrice).toBe(15) // (10 + 20) / 2 + expect(categoryA?.sumPrice).toBe(30) // 10 + 20 + expect(categoryA?.itemCount).toBe(2) + expect(categoryA?.medianPrice).toBe(15) // (10 + 20) / 2 + + const categoryB = result.get(`B`) + expect(categoryB?.avgPrice).toBe(20) // (15 + 25) / 2 + expect(categoryB?.sumPrice).toBe(40) // 15 + 25 + expect(categoryB?.itemCount).toBe(2) + expect(categoryB?.medianPrice).toBe(20) // (15 + 25) / 2 + }) + }) +}) diff --git a/packages/db/tests/query/compiler/custom-operators.test-d.ts b/packages/db/tests/query/compiler/custom-operators.test-d.ts new file mode 100644 index 000000000..5f6269e12 --- /dev/null +++ b/packages/db/tests/query/compiler/custom-operators.test-d.ts @@ -0,0 +1,662 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { createCollection } from '../../../src/collection/index.js' +import { + Func, + add, + comparison, + createLiveQueryCollection, + defineOperator, + eq, + gt, + isUnknown, + numeric, + transform, +} from '../../../src/query/index.js' +import { PropRef, Value } from '../../../src/query/ir.js' +import { mockSyncCollectionOptions } from '../../utils.js' +import type { + CompiledArgsFor, + ExpressionArg, + ExpressionArgs, + TypedCompiledExpression, + TypedEvaluatorFactory, +} from '../../../src/query/index.js' +import type { + CompiledExpression, + EvaluatorFactory, +} from '../../../src/query/ir.js' +import type { RefLeaf } from '../../../src/query/builder/types.js' + +// Sample data type for tests +type TestItem = { + id: number + name: string + value: number + category: string + active: boolean +} + +const sampleItems: Array = [ + { id: 1, name: `Alpha`, value: 10, category: `A`, active: true }, + { id: 2, name: `Beta`, value: 25, category: `A`, active: false }, +] + +function createTestCollection() { + return createCollection( + mockSyncCollectionOptions({ + id: `test-custom-operators-types`, + getKey: (item) => item.id, + initialData: sampleItems, + }), + ) +} + +describe(`Custom Operator Types`, () => { + describe(`defineOperator return types`, () => { + test(`defineOperator returns a function that produces Func`, () => { + // With fully typed TArgs, evaluate callback is typed + const between = defineOperator< + boolean, + [value: number, min: number, max: number] + >({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + if (isUnknown(value)) return null + return value >= minArg(data) && value <= maxArg(data) + }, + }) + + // The returned function should return Func + expectTypeOf(between).toBeFunction() + expectTypeOf(between).returns.toEqualTypeOf>() + }) + + test(`defineOperator with explicit return type and transform helper`, () => { + // Use TArgs with transform helper + const double = defineOperator({ + name: `double`, + compile: transform((v) => v * 2), + }) + + expectTypeOf(double).returns.toEqualTypeOf>() + }) + + test(`defineOperator with string return type and transform helper`, () => { + const prefix = defineOperator({ + name: `prefix`, + compile: transform((v) => `prefix_${v}`), + }) + + expectTypeOf(prefix).returns.toEqualTypeOf>() + }) + + test(`defineOperator with union return type`, () => { + const maybe = defineOperator({ + name: `maybe`, + compile: + ([arg]) => + (data) => { + const value = arg(data) + if (isUnknown(value)) return null + return Boolean(value) + }, + }) + + expectTypeOf(maybe).returns.toEqualTypeOf>() + }) + + test(`defineOperator without generic defaults to unknown`, () => { + const generic = defineOperator({ + name: `generic`, + compile: + ([arg]) => + (data) => + arg!(data), + }) + + expectTypeOf(generic).returns.toEqualTypeOf>() + }) + }) + + describe(`defineOperator argument types`, () => { + test(`typed operator with TArgs generic`, () => { + // Define with explicit argument types + const between = defineOperator({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + return value >= minArg(data) && value <= maxArg(data) + }, + }) + + // Should accept literal numbers + const result = between(5, 1, 10) + expectTypeOf(result).toEqualTypeOf>() + + // Should also accept RefLeaf (from query context) + const refResult = between({} as RefLeaf, 1, {} as RefLeaf) + expectTypeOf(refResult).toEqualTypeOf>() + }) + + test(`typed operator with string arguments`, () => { + const startsWith = defineOperator({ + name: `startsWith`, + compile: + ([strArg, prefixArg]) => + (data) => { + const str = strArg(data) + const prefix = prefixArg(data) + return typeof str === `string` && str.startsWith(prefix) + }, + }) + + // Should accept string literals + const result = startsWith(`hello`, `he`) + expectTypeOf(result).toEqualTypeOf>() + + // Should also accept RefLeaf + const refResult = startsWith({} as RefLeaf, `prefix`) + expectTypeOf(refResult).toEqualTypeOf>() + }) + + test(`ExpressionArg type allows value or expression`, () => { + // ExpressionArg should accept: + type NumArg = ExpressionArg + + // - literal number + expectTypeOf().toMatchTypeOf() + // - RefLeaf + expectTypeOf>().toMatchTypeOf() + // - Func + expectTypeOf>().toMatchTypeOf() + }) + + test(`ExpressionArgs maps tuple of types`, () => { + type Args = ExpressionArgs<[number, string, boolean]> + + // Should be a tuple of ExpressionArg types + expectTypeOf().toMatchTypeOf< + [ExpressionArg, ExpressionArg, ExpressionArg] + >() + }) + + test(`operator accepts literal number arguments`, () => { + // Fully typed with TArgs - evaluate callback gets typed args + const between = defineOperator< + boolean, + [value: number, min: number, max: number] + >({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + return value >= minArg(data) && value <= maxArg(data) + }, + }) + + // Should accept literal numbers + const result = between(5, 1, 10) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`operator accepts literal string arguments`, () => { + // Fully typed with TArgs + const startsWith = defineOperator( + { + name: `startsWith`, + compile: + ([strArg, prefixArg]) => + (data) => { + const str = strArg(data) + const prefix = prefixArg(data) + return str.startsWith(prefix) + }, + }, + ) + + // Should accept literal strings + const result = startsWith(`hello`, `he`) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`operator accepts Value IR nodes as arguments`, () => { + // Fully typed with TArgs + const double = defineOperator({ + name: `double`, + compile: transform((v) => v * 2), + }) + + // Should accept Value nodes + const result = double(new Value(10)) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`operator accepts PropRef IR nodes as arguments`, () => { + // Fully typed with TArgs + const isPositive = defineOperator({ + name: `isPositive`, + compile: + ([arg]) => + (data) => + arg(data) > 0, + }) + + // Should accept PropRef nodes + const result = isPositive(new PropRef([`users`, `age`])) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`operator accepts other Func expressions as arguments`, () => { + const isTrue = defineOperator({ + name: `isTrue`, + compile: + ([arg]) => + (data) => + arg!(data) === true, + }) + + // Should accept other Func expressions (nested operators) + const nestedResult = isTrue(gt(new Value(5), new Value(3))) + expectTypeOf(nestedResult).toEqualTypeOf>() + }) + + test(`operator accepts mixed argument types`, () => { + // Fully typed with TArgs + const between = defineOperator< + boolean, + [value: number, min: number, max: number] + >({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + return value >= minArg(data) && value <= maxArg(data) + }, + }) + + // Should accept mix of PropRef, literal, and Value + const result = between(new PropRef([`items`, `value`]), 10, new Value(20)) + expectTypeOf(result).toEqualTypeOf>() + }) + + test(`operator function accepts any number of arguments`, () => { + const concat = defineOperator({ + name: `concat`, + compile: (args) => (data) => args.map((a) => a(data)).join(``), + }) + + // Should accept variable number of arguments + const result1 = concat(`a`) + const result2 = concat(`a`, `b`) + const result3 = concat(`a`, `b`, `c`, `d`, `e`) + + expectTypeOf(result1).toEqualTypeOf>() + expectTypeOf(result2).toEqualTypeOf>() + expectTypeOf(result3).toEqualTypeOf>() + }) + }) + + describe(`defineOperator evaluate callback types`, () => { + test(`evaluate callback receives typed CompiledArgsFor`, () => { + defineOperator({ + name: `between`, + compile: (compiledArgs) => { + // compiledArgs should be typed tuple with named elements + expectTypeOf(compiledArgs).toEqualTypeOf< + CompiledArgsFor<[value: number, min: number, max: number]> + >() + + // Destructuring preserves names + const [value, min, max] = compiledArgs + + // Each is TypedCompiledExpression + expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(min).toEqualTypeOf>() + expectTypeOf(max).toEqualTypeOf>() + + return (data) => { + // Calling the compiled expressions returns the typed value + const v = value(data) + expectTypeOf(v).toEqualTypeOf() + return true + } + }, + }) + }) + + test(`evaluate callback with string args is typed`, () => { + defineOperator({ + name: `startsWith`, + compile: ([str, prefix]) => { + // Both are TypedCompiledExpression + expectTypeOf(str).toEqualTypeOf>() + expectTypeOf(prefix).toEqualTypeOf>() + + return (data) => { + // Calling returns string + const s = str(data) + const p = prefix(data) + expectTypeOf(s).toEqualTypeOf() + expectTypeOf(p).toEqualTypeOf() + return s.startsWith(p) + } + }, + }) + }) + + test(`evaluate callback second parameter is isSingleRow boolean`, () => { + defineOperator({ + name: `test`, + compile: (_compiledArgs, isSingleRow) => { + expectTypeOf(isSingleRow).toEqualTypeOf() + return () => true + }, + }) + }) + + test(`TypedEvaluatorFactory type is correctly structured`, () => { + type MyFactory = TypedEvaluatorFactory<[a: number, b: string]> + + // Should be a function that takes typed args + expectTypeOf().toMatchTypeOf< + ( + args: CompiledArgsFor<[a: number, b: string]>, + isSingleRow: boolean, + ) => CompiledExpression + >() + }) + + test(`CompiledArgsFor preserves tuple structure`, () => { + type Args = CompiledArgsFor<[value: number, min: number, max: number]> + + // Should be a tuple of typed compiled expressions + expectTypeOf().toEqualTypeOf< + [ + value: TypedCompiledExpression, + min: TypedCompiledExpression, + max: TypedCompiledExpression, + ] + >() + }) + }) + + describe(`factory helper types`, () => { + test(`comparison helper returns TypedEvaluatorFactory`, () => { + // Without type param, use type-safe comparison for unknown + const factory = comparison((a, b) => a === b) + + // comparison returns TypedEvaluatorFactory<[T, T]> + expectTypeOf(factory).toMatchTypeOf< + TypedEvaluatorFactory<[unknown, unknown]> + >() + }) + + test(`comparison with type param is fully typed`, () => { + const factory = comparison((a, b) => { + // a and b are typed as number + expectTypeOf(a).toEqualTypeOf() + expectTypeOf(b).toEqualTypeOf() + return a > b + }) + + expectTypeOf(factory).toMatchTypeOf< + TypedEvaluatorFactory<[number, number]> + >() + }) + + test(`transform helper returns TypedEvaluatorFactory`, () => { + const factory = transform((v) => String(v)) + + expectTypeOf(factory).toMatchTypeOf>() + }) + + test(`transform with type params is fully typed`, () => { + const factory = transform((v) => { + // v is typed as number + expectTypeOf(v).toEqualTypeOf() + return String(v) + }) + + expectTypeOf(factory).toMatchTypeOf>() + }) + + test(`numeric helper returns TypedEvaluatorFactory for numbers`, () => { + const factory = numeric((a, b) => { + // a and b are typed as number + expectTypeOf(a).toEqualTypeOf() + expectTypeOf(b).toEqualTypeOf() + return a + b + }) + + expectTypeOf(factory).toMatchTypeOf< + TypedEvaluatorFactory<[number, number]> + >() + }) + + test(`factories work with defineOperator and typed args`, () => { + // comparison infers types from TArgs + const myGt = defineOperator({ + name: `gt`, + compile: comparison((a, b) => a > b), + }) + expectTypeOf(myGt).returns.toEqualTypeOf>() + + // transform infers type from TArgs + const double = defineOperator({ + name: `double`, + compile: transform((v) => v * 2), + }) + expectTypeOf(double).returns.toEqualTypeOf>() + + // numeric is always [number, number] + const mod = defineOperator({ + name: `mod`, + compile: numeric((a, b) => a % b), + }) + expectTypeOf(mod).returns.toEqualTypeOf>() + }) + + test(`factories type callback params based on TArgs`, () => { + // When used with defineOperator, the callback params should match TArgs + defineOperator({ + name: `gt`, + compile: comparison((a, b) => { + // In this context, a and b should be inferred from usage + return a > b + }), + }) + + defineOperator({ + name: `upper`, + compile: transform((v) => { + // v should be inferred from usage + return v.toUpperCase() + }), + }) + }) + }) + + describe(`custom operators in queries`, () => { + const testCollection = createTestCollection() + + test(`custom operator accepts ref proxies in query context`, () => { + // Fully typed with TArgs + const between = defineOperator< + boolean, + [value: number, min: number, max: number] + >({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + return value >= minArg(data) && value <= maxArg(data) + }, + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + // items.value is a RefLeaf, min/max are literals + .where(({ items }) => between(items.value, 10, 20)) + .select(({ items }) => ({ + id: items.id, + name: items.name, + value: items.value, + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + id: number + name: string + value: number + }> + >() + }) + + test(`custom operator accepts multiple ref proxies`, () => { + // Fully typed with TArgs + const addValues = defineOperator({ + name: `addValues`, + compile: numeric((a, b) => a + b), + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + // Both args are RefLeaf + .select(({ items }) => ({ + id: items.id, + combined: addValues(items.value, items.id), + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + id: number + combined: number + }> + >() + }) + + test(`custom operator accepts string ref proxies`, () => { + // Fully typed with TArgs + const toUpper = defineOperator({ + name: `toUpper`, + compile: transform((v) => v.toUpperCase()), + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + // items.name is RefLeaf + .select(({ items }) => ({ + id: items.id, + upperName: toUpper(items.name), + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + id: number + upperName: string + }> + >() + }) + + test(`custom operator can be nested with built-in operators`, () => { + // Fully typed with TArgs + const double = defineOperator({ + name: `double`, + compile: transform((v) => v * 2), + }) + + // The key test: custom operator can be passed to built-in operator + const doubledValue = double(new Value(10)) + expectTypeOf(doubledValue).toEqualTypeOf>() + + // And built-in operator accepts the custom operator result + // add() returns BasicExpression which can be Func or other expression types + const addedResult = add(doubledValue, new Value(5)) + // Just verify add accepts a Func and returns something + expectTypeOf(addedResult).not.toBeNever() + }) + + test(`built-in operator can be nested inside custom operator`, () => { + // Fully typed with TArgs + const isPositive = defineOperator({ + name: `isPositive`, + compile: + ([arg]) => + (data) => + arg(data) > 0, + }) + + const result = createLiveQueryCollection({ + query: (q) => + q + .from({ items: testCollection }) + // Nest built-in operator inside custom operator + .where(({ items }) => isPositive(add(items.value, items.id))) + .select(({ items }) => ({ + id: items.id, + })), + }) + + expectTypeOf(result.toArray).toEqualTypeOf< + Array<{ + id: number + }> + >() + }) + }) + + describe(`Func IR node types`, () => { + test(`Func type parameter affects node type`, () => { + const booleanFunc = new Func(`test`, []) + expectTypeOf(booleanFunc).toMatchTypeOf>() + + const numberFunc = new Func(`test`, []) + expectTypeOf(numberFunc).toMatchTypeOf>() + + const stringFunc = new Func(`test`, []) + expectTypeOf(stringFunc).toMatchTypeOf>() + }) + + test(`Func with factory is still properly typed`, () => { + // TypedEvaluatorFactory needs to be cast to EvaluatorFactory for direct Func construction + const factory = comparison((a, b) => a === b) + const func = new Func( + `eq`, + [], + factory as unknown as EvaluatorFactory, + ) + + expectTypeOf(func).toMatchTypeOf>() + expectTypeOf(func.name).toEqualTypeOf() + expectTypeOf(func.type).toEqualTypeOf<`func`>() + }) + + test(`Func args array accepts BasicExpression types`, () => { + const func = new Func(`test`, [ + new Value(10), + new PropRef([`users`, `age`]), + eq(new Value(1), new Value(2)), + ]) + + expectTypeOf(func).toMatchTypeOf>() + expectTypeOf(func.args).toBeArray() + }) + }) +}) diff --git a/packages/db/tests/query/compiler/custom-operators.test.ts b/packages/db/tests/query/compiler/custom-operators.test.ts index 80f0ffbe4..95d47fdd1 100644 --- a/packages/db/tests/query/compiler/custom-operators.test.ts +++ b/packages/db/tests/query/compiler/custom-operators.test.ts @@ -3,12 +3,52 @@ import { compileExpression } from '../../../src/query/compiler/evaluators.js' import { Func, PropRef, Value } from '../../../src/query/ir.js' import { toExpression } from '../../../src/query/builder/ref-proxy.js' import { and } from '../../../src/query/builder/operators/index.js' +import { createCollection } from '../../../src/collection/index.js' +import { + comparison, + createLiveQueryCollection, + defineOperator, + isUnknown, + numeric, + transform, +} from '../../../src/query/index.js' +import { mockSyncCollectionOptions } from '../../utils.js' import type { BasicExpression, CompiledExpression, EvaluatorFactory, } from '../../../src/query/ir.js' +// ============================================================ +// Test data for e2e tests +// ============================================================ + +interface TestItem { + id: number + name: string + value: number + category: string + active: boolean +} + +const sampleItems: Array = [ + { id: 1, name: `Alpha`, value: 10, category: `A`, active: true }, + { id: 2, name: `Beta`, value: 25, category: `A`, active: false }, + { id: 3, name: `Gamma`, value: 15, category: `B`, active: true }, + { id: 4, name: `Delta`, value: 30, category: `B`, active: true }, + { id: 5, name: `Epsilon`, value: 5, category: `A`, active: false }, +] + +function createTestCollection() { + return createCollection( + mockSyncCollectionOptions({ + id: `test-custom-operators`, + getKey: (item) => item.id, + initialData: sampleItems, + }), + ) +} + describe(`custom operators`, () => { // Define factory for the "between" operator const betweenFactory: EvaluatorFactory = ( @@ -246,3 +286,203 @@ describe(`custom operators`, () => { }) }) }) + +// ============================================================ +// defineOperator public API tests +// ============================================================ + +describe(`defineOperator public API`, () => { + describe(`typed custom operators`, () => { + it(`between operator with type annotation`, () => { + // Define a "between" operator using the public API with typed args + const between = defineOperator< + boolean, + [value: number, min: number, max: number] + >({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + const min = minArg(data) + const max = maxArg(data) + + if (isUnknown(value)) return null + return value >= min && value <= max + }, + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .where(({ items }) => between(items.value, 10, 20)) + .select(({ items }) => ({ + id: items.id, + name: items.name, + value: items.value, + })), + }) + + // Should include items with value between 10 and 20 (inclusive) + expect(result.size).toBe(2) + expect(result.toArray.map((r) => r.name).sort()).toEqual([ + `Alpha`, + `Gamma`, + ]) + }) + + it(`startsWith operator in a where clause`, () => { + // Define a "startsWith" operator with typed args + const startsWith = defineOperator( + { + name: `startsWith`, + compile: + ([strArg, prefixArg]) => + (data) => { + const str = strArg(data) + const prefix = prefixArg(data) + + if (isUnknown(str) || isUnknown(prefix)) return null + if (typeof str !== `string` || typeof prefix !== `string`) + return false + + return str.startsWith(prefix) + }, + }, + ) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .where(({ items }) => startsWith(items.name, `A`)) + .select(({ items }) => ({ + id: items.id, + name: items.name, + })), + }) + + expect(result.size).toBe(1) + expect(result.get(1)?.name).toBe(`Alpha`) + }) + }) + + describe(`factory helpers with defineOperator`, () => { + it(`notEquals using comparison helper`, () => { + // Define using the comparison helper with typed args + const notEquals = defineOperator({ + name: `notEquals`, + compile: comparison((a, b) => a !== b), + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .where(({ items }) => notEquals(items.category, `A`)) + .select(({ items }) => ({ + id: items.id, + category: items.category, + })), + }) + + expect(result.size).toBe(2) + expect(result.toArray.every((r) => r.category === `B`)).toBe(true) + }) + + it(`double using transform helper`, () => { + // Define using the transform helper with typed args + const double = defineOperator({ + name: `double`, + compile: transform((v) => v * 2), + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q.from({ items: collection }).select(({ items }) => ({ + id: items.id, + doubledValue: double(items.value), + })), + }) + + expect(result.get(1)?.doubledValue).toBe(20) // 10 * 2 + expect(result.get(4)?.doubledValue).toBe(60) // 30 * 2 + }) + + it(`modulo using numeric helper`, () => { + // Define using the numeric helper with typed args + const modulo = defineOperator({ + name: `modulo`, + compile: numeric((a, b) => (b !== 0 ? a % b : null)), + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q.from({ items: collection }).select(({ items }) => ({ + id: items.id, + value: items.value, + mod3: modulo(items.value, 3), + })), + }) + + expect(result.get(1)?.mod3).toBe(1) // 10 % 3 = 1 + expect(result.get(2)?.mod3).toBe(1) // 25 % 3 = 1 + expect(result.get(3)?.mod3).toBe(0) // 15 % 3 = 0 + }) + }) + + describe(`custom operators in complex queries`, () => { + it(`custom operator combined with built-in operators`, () => { + const between = defineOperator< + boolean, + [value: number, min: number, max: number] + >({ + name: `between`, + compile: + ([valueArg, minArg, maxArg]) => + (data) => { + const value = valueArg(data) + return value >= minArg(data) && value <= maxArg(data) + }, + }) + + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .where(({ items }) => between(items.value, 10, 25)) + .where(({ items }) => items.active) + .select(({ items }) => ({ + id: items.id, + name: items.name, + })), + }) + + // Value between 10-25 AND active + expect(result.size).toBe(2) + expect(result.toArray.map((r) => r.name).sort()).toEqual([ + `Alpha`, + `Gamma`, + ]) + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d1727c7b..8fdd5ff60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 0.3.3(typescript@5.9.3) '@tanstack/vite-config': specifier: 0.4.3 - version: 0.4.3(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + version: 0.4.3(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -50,6 +50,9 @@ importers: '@vitejs/plugin-react': specifier: ^5.1.2 version: 5.1.2(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + esbuild: + specifier: ^0.27.2 + version: 0.27.2 eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -184,6 +187,8 @@ importers: specifier: ^5.9.2 version: 5.9.3 + examples/react/linearlarge: {} + examples/react/offline-transactions: dependencies: '@tanstack/offline-transactions': @@ -682,6 +687,8 @@ importers: specifier: ^0.16.0 version: 0.16.0 + packages/benchmarks: {} + packages/db: dependencies: '@standard-schema/spec': @@ -699,7 +706,7 @@ importers: devDependencies: '@tanstack/config': specifier: ^0.22.2 - version: 0.22.2(@types/node@24.7.0)(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + version: 0.22.2(@types/node@24.7.0)(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) '@vitest/coverage-istanbul': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) @@ -747,6 +754,8 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) + packages/db-devtools: {} + packages/db-ivm: dependencies: fractional-indexing: @@ -766,6 +775,8 @@ importers: specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) + packages/db-tracing: {} + packages/electric-db-collection: dependencies: '@electric-sql/client': @@ -900,6 +911,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(react@19.2.3) + packages/rss-db-collection: {} + packages/rxdb-db-collection: dependencies: '@standard-schema/spec': @@ -931,6 +944,8 @@ importers: specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) + packages/shared-types: {} + packages/solid-db: dependencies: '@solid-primitives/map': @@ -1517,8 +1532,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -1541,8 +1556,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -1565,8 +1580,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -1589,8 +1604,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -1613,8 +1628,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -1637,8 +1652,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -1661,8 +1676,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -1685,8 +1700,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -1709,8 +1724,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -1733,8 +1748,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -1757,8 +1772,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1781,8 +1796,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1805,8 +1820,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1829,8 +1844,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1853,8 +1868,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1877,8 +1892,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1901,8 +1916,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -1919,8 +1934,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1943,8 +1958,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1961,8 +1976,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1985,8 +2000,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -2003,8 +2018,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -2027,8 +2042,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -2051,8 +2066,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2075,8 +2090,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2099,8 +2114,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -3192,6 +3207,11 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.52.3': resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} cpu: [arm64] @@ -3202,6 +3222,11 @@ packages: cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.52.3': resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} cpu: [arm64] @@ -3212,6 +3237,11 @@ packages: cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.52.3': resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} cpu: [x64] @@ -3222,6 +3252,11 @@ packages: cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.52.3': resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} cpu: [arm64] @@ -3232,6 +3267,11 @@ packages: cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.52.3': resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} cpu: [x64] @@ -3242,6 +3282,11 @@ packages: cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} cpu: [arm] @@ -3252,6 +3297,11 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.52.3': resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} cpu: [arm] @@ -3262,6 +3312,11 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.52.3': resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} cpu: [arm64] @@ -3272,6 +3327,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.52.3': resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} cpu: [arm64] @@ -3282,6 +3342,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.52.3': resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} cpu: [loong64] @@ -3292,6 +3357,16 @@ packages: cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.52.3': resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} cpu: [ppc64] @@ -3302,6 +3377,16 @@ packages: cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.52.3': resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} cpu: [riscv64] @@ -3312,6 +3397,11 @@ packages: cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.52.3': resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} cpu: [riscv64] @@ -3322,6 +3412,11 @@ packages: cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.52.3': resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} cpu: [s390x] @@ -3332,6 +3427,11 @@ packages: cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.52.3': resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} cpu: [x64] @@ -3342,6 +3442,11 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.52.3': resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} cpu: [x64] @@ -3352,6 +3457,16 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + '@rollup/rollup-openharmony-arm64@4.52.3': resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} cpu: [arm64] @@ -3362,6 +3477,11 @@ packages: cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.52.3': resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} cpu: [arm64] @@ -3372,6 +3492,11 @@ packages: cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.52.3': resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} cpu: [ia32] @@ -3382,6 +3507,11 @@ packages: cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.52.3': resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} cpu: [x64] @@ -3392,6 +3522,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.52.3': resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} cpu: [x64] @@ -3402,6 +3537,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + cpu: [x64] + os: [win32] + '@rushstack/node-core-library@5.7.0': resolution: {integrity: sha512-Ff9Cz/YlWu9ce4dmqNBZpA45AEya04XaBFIjV7xTVeEf+y/kTjEasmozqFELXlNG4ROdevss75JrrZ5WgufDkQ==} peerDependencies: @@ -5402,8 +5542,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} hasBin: true @@ -7575,6 +7715,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rou3@0.5.1: resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} @@ -9506,7 +9651,7 @@ snapshots: '@esbuild/aix-ppc64@0.25.9': optional: true - '@esbuild/aix-ppc64@0.27.0': + '@esbuild/aix-ppc64@0.27.2': optional: true '@esbuild/android-arm64@0.18.20': @@ -9518,7 +9663,7 @@ snapshots: '@esbuild/android-arm64@0.25.9': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/android-arm64@0.27.2': optional: true '@esbuild/android-arm@0.18.20': @@ -9530,7 +9675,7 @@ snapshots: '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm@0.27.2': optional: true '@esbuild/android-x64@0.18.20': @@ -9542,7 +9687,7 @@ snapshots: '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-x64@0.27.2': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -9554,7 +9699,7 @@ snapshots: '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/darwin-arm64@0.27.2': optional: true '@esbuild/darwin-x64@0.18.20': @@ -9566,7 +9711,7 @@ snapshots: '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-x64@0.27.2': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -9578,7 +9723,7 @@ snapshots: '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/freebsd-arm64@0.27.2': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -9590,7 +9735,7 @@ snapshots: '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-x64@0.27.2': optional: true '@esbuild/linux-arm64@0.18.20': @@ -9602,7 +9747,7 @@ snapshots: '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/linux-arm64@0.27.2': optional: true '@esbuild/linux-arm@0.18.20': @@ -9614,7 +9759,7 @@ snapshots: '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm@0.27.2': optional: true '@esbuild/linux-ia32@0.18.20': @@ -9626,7 +9771,7 @@ snapshots: '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-ia32@0.27.2': optional: true '@esbuild/linux-loong64@0.18.20': @@ -9638,7 +9783,7 @@ snapshots: '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-loong64@0.27.2': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -9650,7 +9795,7 @@ snapshots: '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-mips64el@0.27.2': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -9662,7 +9807,7 @@ snapshots: '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-ppc64@0.27.2': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -9674,7 +9819,7 @@ snapshots: '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-riscv64@0.27.2': optional: true '@esbuild/linux-s390x@0.18.20': @@ -9686,7 +9831,7 @@ snapshots: '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-s390x@0.27.2': optional: true '@esbuild/linux-x64@0.18.20': @@ -9698,7 +9843,7 @@ snapshots: '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-x64@0.27.2': optional: true '@esbuild/netbsd-arm64@0.25.11': @@ -9707,7 +9852,7 @@ snapshots: '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/netbsd-arm64@0.27.2': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -9719,7 +9864,7 @@ snapshots: '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-x64@0.27.2': optional: true '@esbuild/openbsd-arm64@0.25.11': @@ -9728,7 +9873,7 @@ snapshots: '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/openbsd-arm64@0.27.2': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -9740,7 +9885,7 @@ snapshots: '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-x64@0.27.2': optional: true '@esbuild/openharmony-arm64@0.25.11': @@ -9749,7 +9894,7 @@ snapshots: '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openharmony-arm64@0.27.2': optional: true '@esbuild/sunos-x64@0.18.20': @@ -9761,7 +9906,7 @@ snapshots: '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/sunos-x64@0.27.2': optional: true '@esbuild/win32-arm64@0.18.20': @@ -9773,7 +9918,7 @@ snapshots: '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/win32-arm64@0.27.2': optional: true '@esbuild/win32-ia32@0.18.20': @@ -9785,7 +9930,7 @@ snapshots: '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-ia32@0.27.2': optional: true '@esbuild/win32-x64@0.18.20': @@ -9797,7 +9942,7 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-x64@0.27.2': optional: true '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': @@ -10968,13 +11113,13 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.53': {} - '@rollup/pluginutils@5.3.0(rollup@4.52.5)': + '@rollup/pluginutils@5.3.0(rollup@4.55.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.5 + rollup: 4.55.1 '@rollup/rollup-android-arm-eabi@4.52.3': optional: true @@ -10982,132 +11127,207 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.52.5': optional: true + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + '@rollup/rollup-android-arm64@4.52.3': optional: true '@rollup/rollup-android-arm64@4.52.5': optional: true + '@rollup/rollup-android-arm64@4.55.1': + optional: true + '@rollup/rollup-darwin-arm64@4.52.3': optional: true '@rollup/rollup-darwin-arm64@4.52.5': optional: true + '@rollup/rollup-darwin-arm64@4.55.1': + optional: true + '@rollup/rollup-darwin-x64@4.52.3': optional: true '@rollup/rollup-darwin-x64@4.52.5': optional: true + '@rollup/rollup-darwin-x64@4.55.1': + optional: true + '@rollup/rollup-freebsd-arm64@4.52.3': optional: true '@rollup/rollup-freebsd-arm64@4.52.5': optional: true + '@rollup/rollup-freebsd-arm64@4.55.1': + optional: true + '@rollup/rollup-freebsd-x64@4.52.3': optional: true '@rollup/rollup-freebsd-x64@4.52.5': optional: true + '@rollup/rollup-freebsd-x64@4.55.1': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': optional: true '@rollup/rollup-linux-arm-gnueabihf@4.52.5': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.52.3': optional: true '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.52.3': optional: true '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-arm64-gnu@4.55.1': + optional: true + '@rollup/rollup-linux-arm64-musl@4.52.3': optional: true '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true + '@rollup/rollup-linux-arm64-musl@4.55.1': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.52.3': optional: true '@rollup/rollup-linux-loong64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-loong64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.1': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.52.3': optional: true '@rollup/rollup-linux-ppc64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.52.3': optional: true '@rollup/rollup-linux-riscv64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.52.3': optional: true '@rollup/rollup-linux-riscv64-musl@4.52.5': optional: true + '@rollup/rollup-linux-riscv64-musl@4.55.1': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.52.3': optional: true '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true + '@rollup/rollup-linux-s390x-gnu@4.55.1': + optional: true + '@rollup/rollup-linux-x64-gnu@4.52.3': optional: true '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-x64-gnu@4.55.1': + optional: true + '@rollup/rollup-linux-x64-musl@4.52.3': optional: true '@rollup/rollup-linux-x64-musl@4.52.5': optional: true + '@rollup/rollup-linux-x64-musl@4.55.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.1': + optional: true + '@rollup/rollup-openharmony-arm64@4.52.3': optional: true '@rollup/rollup-openharmony-arm64@4.52.5': optional: true + '@rollup/rollup-openharmony-arm64@4.55.1': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.52.3': optional: true '@rollup/rollup-win32-arm64-msvc@4.52.5': optional: true + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.52.3': optional: true '@rollup/rollup-win32-ia32-msvc@4.52.5': optional: true + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + '@rollup/rollup-win32-x64-gnu@4.52.3': optional: true '@rollup/rollup-win32-x64-gnu@4.52.5': optional: true + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + '@rollup/rollup-win32-x64-msvc@4.52.3': optional: true '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@rollup/rollup-win32-x64-msvc@4.55.1': + optional: true + '@rushstack/node-core-library@5.7.0(@types/node@24.7.0)': dependencies: ajv: 8.13.0 @@ -11466,12 +11686,12 @@ snapshots: tailwindcss: 4.1.18 vite: 7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) - '@tanstack/config@0.22.2(@types/node@24.7.0)(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))': + '@tanstack/config@0.22.2(@types/node@24.7.0)(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@tanstack/eslint-config': 0.3.3(@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@tanstack/publish-config': 0.2.2 '@tanstack/typedoc-config': 0.3.2(typescript@5.9.3) - '@tanstack/vite-config': 0.4.1(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + '@tanstack/vite-config': 0.4.1(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) transitivePeerDependencies: - '@types/node' - '@typescript-eslint/utils' @@ -11818,10 +12038,10 @@ snapshots: '@tanstack/virtual-file-routes@1.145.4': {} - '@tanstack/vite-config@0.4.1(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))': + '@tanstack/vite-config@0.4.1(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))': dependencies: - rollup-plugin-preserve-directives: 0.4.0(rollup@4.52.5) - vite-plugin-dts: 4.2.3(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + rollup-plugin-preserve-directives: 0.4.0(rollup@4.55.1) + vite-plugin-dts: 4.2.3(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) vite-plugin-externalize-deps: 0.10.0(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) transitivePeerDependencies: @@ -11831,11 +12051,11 @@ snapshots: - typescript - vite - '@tanstack/vite-config@0.4.3(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))': + '@tanstack/vite-config@0.4.3(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))': dependencies: - rollup-plugin-preserve-directives: 0.4.0(rollup@4.52.5) + rollup-plugin-preserve-directives: 0.4.0(rollup@4.55.1) vite: 7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) - vite-plugin-dts: 4.2.3(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + vite-plugin-dts: 4.2.3(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) vite-plugin-externalize-deps: 0.10.0(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) transitivePeerDependencies: @@ -13583,34 +13803,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 - esbuild@0.27.0: + esbuild@0.27.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} @@ -15958,11 +16178,11 @@ snapshots: dependencies: glob: 7.2.3 - rollup-plugin-preserve-directives@0.4.0(rollup@4.52.5): + rollup-plugin-preserve-directives@0.4.0(rollup@4.55.1): dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) + '@rollup/pluginutils': 5.3.0(rollup@4.55.1) magic-string: 0.30.21 - rollup: 4.52.5 + rollup: 4.55.1 rollup@4.52.3: dependencies: @@ -16020,6 +16240,37 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + rollup@4.55.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 + fsevents: 2.3.3 + rou3@0.5.1: {} rou3@0.7.12: {} @@ -16797,7 +17048,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.0 + esbuild: 0.27.2 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -17027,10 +17278,10 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.2.3(@types/node@24.7.0)(rollup@4.52.5)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)): + vite-plugin-dts@4.2.3(@types/node@24.7.0)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.47.7(@types/node@24.7.0) - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) + '@rollup/pluginutils': 5.3.0(rollup@4.55.1) '@volar/typescript': 2.4.23 '@vue/language-core': 2.1.6(typescript@5.9.3) compare-versions: 6.1.1 @@ -17096,11 +17347,11 @@ snapshots: vite@7.3.0(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1): dependencies: - esbuild: 0.27.0 + esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.5 + rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.7.0