UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

775 lines (700 loc) • 23.7 kB
import { Aggregate, Func, isBasicOrAggregateExpression } from '../ir' import { codedMessage, devBuild } from '../../error-message.js' import { grouping } from '../compiler/grouping.js' import { isRefProxy, toExpression } from './ref-proxy.js' import { registerWrapper } from './wrapper-identity.js' import type { BasicExpression } from '../ir' import type { IncludesRuntime } from '../live/includes-runtime.js' import type { RefProxy } from './ref-proxy.js' import type { ContainsAggregate, Context, RefLeaf, StringifiableScalar, } from './types.js' import type { BaseQueryBuilder, QueryBuilder } from './index.js' // Aggregate provenance is a type-only fact. Wrapping an expression must not // hide the implicit group from a downstream QueryRef's result type. type WithAggregate<TResult, TInputs> = true extends ContainsAggregate<TInputs> ? TResult & { readonly __hasAggregate: true } : TResult type StringRef = RefLeaf<string> | RefLeaf<string | null> | RefLeaf<string | undefined> type StringRefProxy = RefProxy<string> | RefProxy<string | null> | RefProxy<string | undefined> type StringBasicExpression = | BasicExpression<string> | BasicExpression<string | null> | BasicExpression<string | undefined> type StringLike = StringRef | StringRefProxy | StringBasicExpression | string | null | undefined type ComparisonOperand<T> = RefProxy<T> | RefLeaf<T> | T | BasicExpression<T> | undefined | null type ComparisonOperandPrimitive<T extends string | number | boolean> = T | BasicExpression<T> | undefined | null // Helper type for values that can be lowered to expressions. type ExpressionLike = | Aggregate | BasicExpression | RefProxy<any> | RefLeaf<any> | string | number | boolean | bigint | Date | null | undefined | Array<unknown> export type CaseWhenValue = | ExpressionLike | IncludeWrapper<any> | ToArrayWrapper<any> | ConcatToArrayWrapper<any> // A select object. A query builder is not one: wrap it in include(). | (Record<string, any> & { _bindCollectionSources?: never }) type ExtractCaseWhenValue<T> = T extends CaseWhenWrapper<infer TResult> ? TResult : T type CaseWhenResult< TValues extends Array<CaseWhenValue>, THasDefault extends boolean, TConditions = never, > = WithAggregate< TValues[number] extends ExpressionLike ? BasicExpression< ExtractType<TValues[number]> | (THasDefault extends true ? never : null) > : CaseWhenWrapper< | ExtractCaseWhenValue<TValues[number]> | (THasDefault extends true ? never : undefined) >, [TValues, TConditions] > // Keep the first six conditions expression-like, as the former overloads did. // Beyond five pairs the public result retains its existing any fallback. type CaseWhenInputs< T extends Array<CaseWhenValue>, TPairs extends Array<unknown> = [], > = TPairs[`length`] extends 6 ? T : T extends [unknown, unknown, ...infer Rest extends Array<CaseWhenValue>] ? [ExpressionLike, CaseWhenValue, ...CaseWhenInputs<Rest, [...TPairs, 0]>] : T type CaseWhenOutput< T extends Array<CaseWhenValue>, TValues extends Array<CaseWhenValue> = [], TConditions extends Array<ExpressionLike> = [], > = T extends [infer Default extends CaseWhenValue] ? CaseWhenResult<[...TValues, Default], true, TConditions> : T extends [] ? CaseWhenResult<TValues, false, TConditions> : TConditions[`length`] extends 5 ? any : T extends [ infer Condition extends ExpressionLike, infer Value extends CaseWhenValue, ...infer Rest extends Array<CaseWhenValue>, ] ? CaseWhenOutput<Rest, [...TValues, Value], [...TConditions, Condition]> : never // Helper type to extract the underlying type from various expression types type ExtractType<T> = T extends RefProxy<infer U> ? U : T extends RefLeaf<infer U> ? U : T extends BasicExpression<infer U> | Aggregate<infer U> ? U : T type IsAny<T> = 0 extends 1 & T ? true : false type AggregateArgument<T, Domain> = T & (IsAny<ExtractType<T>> extends true ? unknown : [Exclude<ExtractType<T>, null | undefined>] extends [never] ? never : [Exclude<ExtractType<T>, null | undefined>] extends [Domain] ? unknown : never) type OrderableAggregateValue = number | Date | bigint | string type AggregateWrapper<T> = RefProxy<T> | RefLeaf<T> | BasicExpression<T> // Constrained overloads compose through supported generics; these conditional // fallbacks validate concrete optional/nullish unions and reject unknown. type NumericAggregateWrapperArgument<T> = AggregateArgument< AggregateWrapper<T>, number > type OrderableAggregateWrapperArgument<T> = AggregateArgument< AggregateWrapper<T>, OrderableAggregateValue > type NumericAggregateArgument<T> = AggregateArgument<T, number> type OrderableAggregateArgument<T> = AggregateArgument< T, OrderableAggregateValue > // Helper type to determine string function return type based on input nullability type StringFunctionReturnType<T> = ExtractType<T> extends infer U ? U extends string | undefined | null ? BasicExpression<U> : BasicExpression<string | undefined | null> : BasicExpression<string | undefined | null> // Helper type to determine numeric function return type based on input nullability // This handles string, array, and number inputs for functions like length() type NumericFunctionReturnType<T> = ExtractType<T> extends infer U ? U extends string | Array<any> | undefined | null | number ? BasicExpression<MapToNumber<U>> : BasicExpression<number | undefined | null> : BasicExpression<number | undefined | null> // Transform string/array types to number while preserving nullability type MapToNumber<T> = T extends string | Array<any> ? number : T extends undefined ? undefined : T extends null ? null : T // Helper type for binary numeric operations. // Runtime coalesces nullish operands to 0 for these operations, so nullable // operands don't make the result nullable. type BinaryNumericReturnType = BasicExpression<number> type DivideReturnType = BasicExpression<number | null> // Operators export function eq<L extends ExpressionLike, R extends ExpressionLike>( left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never), right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function eq<T>( left: ComparisonOperand<T>, right: ComparisonOperand<T>, ): BasicExpression<boolean> export function eq<T extends string | number | boolean>( left: ComparisonOperandPrimitive<T>, right: ComparisonOperandPrimitive<T>, ): BasicExpression<boolean> export function eq<T>(left: Aggregate<T>, right: any): BasicExpression<boolean> export function eq(left: any, right: any): BasicExpression<boolean> { return new Func(`eq`, [toExpression(left), toExpression(right)]) } export function gt<L extends ExpressionLike, R extends ExpressionLike>( left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never), right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function gt<T>( left: ComparisonOperand<T>, right: ComparisonOperand<T>, ): BasicExpression<boolean> export function gt<T extends string | number>( left: ComparisonOperandPrimitive<T>, right: ComparisonOperandPrimitive<T>, ): BasicExpression<boolean> export function gt<T>(left: Aggregate<T>, right: any): BasicExpression<boolean> export function gt(left: any, right: any): BasicExpression<boolean> { return new Func(`gt`, [toExpression(left), toExpression(right)]) } export function gte<L extends ExpressionLike, R extends ExpressionLike>( left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never), right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function gte<T>( left: ComparisonOperand<T>, right: ComparisonOperand<T>, ): BasicExpression<boolean> export function gte<T extends string | number>( left: ComparisonOperandPrimitive<T>, right: ComparisonOperandPrimitive<T>, ): BasicExpression<boolean> export function gte<T>(left: Aggregate<T>, right: any): BasicExpression<boolean> export function gte(left: any, right: any): BasicExpression<boolean> { return new Func(`gte`, [toExpression(left), toExpression(right)]) } export function lt<L extends ExpressionLike, R extends ExpressionLike>( left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never), right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function lt<T>( left: ComparisonOperand<T>, right: ComparisonOperand<T>, ): BasicExpression<boolean> export function lt<T extends string | number>( left: ComparisonOperandPrimitive<T>, right: ComparisonOperandPrimitive<T>, ): BasicExpression<boolean> export function lt<T>(left: Aggregate<T>, right: any): BasicExpression<boolean> export function lt(left: any, right: any): BasicExpression<boolean> { return new Func(`lt`, [toExpression(left), toExpression(right)]) } export function lte<L extends ExpressionLike, R extends ExpressionLike>( left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never), right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function lte<T>( left: ComparisonOperand<T>, right: ComparisonOperand<T>, ): BasicExpression<boolean> export function lte<T extends string | number>( left: ComparisonOperandPrimitive<T>, right: ComparisonOperandPrimitive<T>, ): BasicExpression<boolean> export function lte<T>(left: Aggregate<T>, right: any): BasicExpression<boolean> export function lte(left: any, right: any): BasicExpression<boolean> { return new Func(`lte`, [toExpression(left), toExpression(right)]) } // Overloads for and() - support 2 or more arguments export function and< T extends [ExpressionLike, ExpressionLike, ...Array<ExpressionLike>], >(...args: T): WithAggregate<BasicExpression<boolean>, T> export function and( left: ExpressionLike, right: ExpressionLike, ...rest: Array<ExpressionLike> ): BasicExpression<boolean> { const allArgs = [left, right, ...rest] return new Func( `and`, allArgs.map((arg) => toExpression(arg)), ) } // Overloads for or() - support 2 or more arguments export function or< T extends [ExpressionLike, ExpressionLike, ...Array<ExpressionLike>], >(...args: T): WithAggregate<BasicExpression<boolean>, T> export function or( left: ExpressionLike, right: ExpressionLike, ...rest: Array<ExpressionLike> ): BasicExpression<boolean> { const allArgs = [left, right, ...rest] return new Func( `or`, allArgs.map((arg) => toExpression(arg)), ) } export function not<T extends ExpressionLike>( value: T, ): WithAggregate<BasicExpression<boolean>, T> export function not(value: ExpressionLike): BasicExpression<boolean> { return new Func(`not`, [toExpression(value)]) } // Null/undefined checking functions export function isUndefined<T extends ExpressionLike>( value: T, ): WithAggregate<BasicExpression<boolean>, T> export function isUndefined(value: ExpressionLike): BasicExpression<boolean> { return new Func(`isUndefined`, [toExpression(value)]) } export function isNull<T extends ExpressionLike>( value: T, ): WithAggregate<BasicExpression<boolean>, T> export function isNull(value: ExpressionLike): BasicExpression<boolean> { return new Func(`isNull`, [toExpression(value)]) } export function inArray<L extends ExpressionLike, R extends ExpressionLike>( left: L, right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function inArray( value: ExpressionLike, array: ExpressionLike, ): BasicExpression<boolean> { return new Func(`in`, [toExpression(value), toExpression(array)]) } export function like<L extends StringLike, R extends StringLike>( left: L, right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function like( left: StringLike, right: StringLike, ): BasicExpression<boolean> export function like(left: any, right: any): BasicExpression<boolean> { return new Func(`like`, [toExpression(left), toExpression(right)]) } export function ilike<L extends StringLike, R extends StringLike>( left: L, right: R, ): WithAggregate<BasicExpression<boolean>, [L, R]> export function ilike( left: StringLike, right: StringLike, ): BasicExpression<boolean> { return new Func(`ilike`, [toExpression(left), toExpression(right)]) } // Functions export function upper<T extends ExpressionLike>( arg: T, ): WithAggregate<StringFunctionReturnType<T>, T> { return new Func(`upper`, [toExpression(arg)]) as WithAggregate< StringFunctionReturnType<T>, T > } export function lower<T extends ExpressionLike>( arg: T, ): WithAggregate<StringFunctionReturnType<T>, T> { return new Func(`lower`, [toExpression(arg)]) as WithAggregate< StringFunctionReturnType<T>, T > } export function length<T extends ExpressionLike>( arg: T, ): WithAggregate<NumericFunctionReturnType<T>, T> { return new Func(`length`, [toExpression(arg)]) as WithAggregate< NumericFunctionReturnType<T>, T > } export function concat<T extends StringifiableScalar>( arg: ToArrayWrapper<T>, ): ConcatToArrayWrapper<T> export function concat<T extends Array<ExpressionLike>>( ...args: T ): WithAggregate<BasicExpression<string>, T> export function concat( ...args: Array<ExpressionLike | ToArrayWrapper<any>> ): BasicExpression<string> | ConcatToArrayWrapper<any> { const toArrayArg = args.find( (arg): arg is ToArrayWrapper<any> => arg instanceof ToArrayWrapper, ) if (toArrayArg) { if (args.length !== 1) { throw new Error( devBuild() && process.env.NODE_ENV !== `production` ? `concat(toArray(...)) currently supports only a single toArray(...) argument` : codedMessage(110), ) } return new ConcatToArrayWrapper(toArrayArg.query, toArrayArg.runtime) } return new Func( `concat`, args.map((arg) => toExpression(arg)), ) } // Helper type for coalesce: extracts non-nullish value types from all args type CoalesceArgTypes<T extends Array<ExpressionLike>> = { [K in keyof T]: NonNullable<ExtractType<T[K]>> }[number] // Whether any arg in the tuple is statically guaranteed non-null (i.e., does not include null | undefined) type HasGuaranteedNonNull<T extends Array<ExpressionLike>> = { [K in keyof T]: null extends ExtractType<T[K]> ? false : undefined extends ExtractType<T[K]> ? false : true }[number] extends false ? false : true // coalesce() return type: union of all non-null arg types; null included unless a guaranteed non-null arg exists type CoalesceReturnType<T extends Array<ExpressionLike>> = WithAggregate< HasGuaranteedNonNull<T> extends true ? BasicExpression<CoalesceArgTypes<T>> : BasicExpression<CoalesceArgTypes<T> | null>, T > export function coalesce<T extends [ExpressionLike, ...Array<ExpressionLike>]>( ...args: T ): CoalesceReturnType<T> { return new Func( `coalesce`, args.map((arg) => toExpression(arg)), ) as CoalesceReturnType<T> } /** * Returns the value for the first matching condition, similar to SQL * `CASE WHEN`. * * Arguments are evaluated as condition/value pairs followed by an optional * default value. Scalar branch values return a query expression and can be used * in expression contexts like `select`, `where`, `orderBy`, `groupBy`, * `having`, and equality join operands. If no scalar branch matches and no * default is provided, the result is `null`. * * When a branch value is a projection object, `caseWhen` becomes a select-only * projection value. Projection branches can include nested fields, ref spreads, * and includes. If no projection branch matches and no default is provided, the * result is `undefined`. * * @example * ```ts * caseWhen(gt(user.age, 18), `adult`, `minor`) * ``` * * @example * ```ts * caseWhen( * gt(user.age, 65), * `senior`, * gt(user.age, 18), * `adult`, * `minor`, * ) * ``` * * @example * ```ts * caseWhen(gt(user.age, 18), { * ...user, * posts: include( * q * .from({ post: postsCollection }) * .where(({ post }) => eq(post.userId, user.id)), * ), * }) * ``` */ export function caseWhen< T extends [ExpressionLike, CaseWhenValue, ...Array<CaseWhenValue>], >(...args: T & CaseWhenInputs<T>): CaseWhenOutput<T> export function caseWhen(...args: Array<CaseWhenValue>): any { if (args.length < 2) { throw new Error( devBuild() && process.env.NODE_ENV !== `production` ? `caseWhen() requires at least two arguments` : codedMessage(111), ) } const pairCount = Math.floor(args.length / 2) for (let i = 0; i < pairCount; i++) { const condition = args[i * 2] if (!isConditionValue(condition)) { throw new Error( devBuild() && process.env.NODE_ENV !== `production` ? `caseWhen() conditions must be expression-like values` : codedMessage(112), ) } } if (caseWhenHasOnlyExpressionValues(args)) { return new Func( `caseWhen`, args.map((arg) => toExpression(arg)), ) } return new CaseWhenWrapper(args) } export function add<T1 extends ExpressionLike, T2 extends ExpressionLike>( left: T1, right: T2, ): WithAggregate<BinaryNumericReturnType, [T1, T2]> { return new Func(`add`, [ toExpression(left), toExpression(right), ]) as WithAggregate<BinaryNumericReturnType, [T1, T2]> } export function subtract<T1 extends ExpressionLike, T2 extends ExpressionLike>( left: T1, right: T2, ): WithAggregate<BinaryNumericReturnType, [T1, T2]> { return new Func(`subtract`, [ toExpression(left), toExpression(right), ]) as WithAggregate<BinaryNumericReturnType, [T1, T2]> } export function multiply<T1 extends ExpressionLike, T2 extends ExpressionLike>( left: T1, right: T2, ): WithAggregate<BinaryNumericReturnType, [T1, T2]> { return new Func(`multiply`, [ toExpression(left), toExpression(right), ]) as WithAggregate<BinaryNumericReturnType, [T1, T2]> } export function divide<T1 extends ExpressionLike, T2 extends ExpressionLike>( left: T1, right: T2, ): WithAggregate<DivideReturnType, [T1, T2]> { return new Func(`divide`, [ toExpression(left), toExpression(right), ]) as WithAggregate<DivideReturnType, [T1, T2]> } // Aggregates export function count(arg: ExpressionLike): Aggregate<number> { return new Aggregate(`count`, [toExpression(arg)], grouping) } export function avg<T extends number>(arg: T): Aggregate<number> export function avg<T>( arg: NumericAggregateWrapperArgument<T>, ): Aggregate<number> export function avg<T extends ExpressionLike>( arg: NumericAggregateArgument<T>, ): Aggregate<number> export function avg(arg: ExpressionLike): Aggregate<number> { return new Aggregate(`avg`, [toExpression(arg)], grouping) } export function sum<T extends number>(arg: T): Aggregate<number> export function sum<T>( arg: NumericAggregateWrapperArgument<T>, ): Aggregate<number> export function sum<T extends ExpressionLike>( arg: NumericAggregateArgument<T>, ): Aggregate<number> export function sum(arg: ExpressionLike): Aggregate<number> { return new Aggregate(`sum`, [toExpression(arg)], grouping) } export function min<T extends OrderableAggregateValue>(arg: T): Aggregate<T> export function min<T>(arg: OrderableAggregateWrapperArgument<T>): Aggregate<T> export function min<T extends ExpressionLike>( arg: OrderableAggregateArgument<T>, ): Aggregate<ExtractType<T>> export function min(arg: ExpressionLike): Aggregate { return new Aggregate(`min`, [toExpression(arg)], grouping) } export function max<T extends OrderableAggregateValue>(arg: T): Aggregate<T> export function max<T>(arg: OrderableAggregateWrapperArgument<T>): Aggregate<T> export function max<T extends ExpressionLike>( arg: OrderableAggregateArgument<T>, ): Aggregate<ExtractType<T>> export function max(arg: ExpressionLike): Aggregate { return new Aggregate(`max`, [toExpression(arg)], grouping) } /** * List of comparison function names that can be used with indexes */ export const indexableOperations = [ `eq`, `gt`, `gte`, `lt`, `lte`, `in`, ] as const export const comparisonFunctions = [ ...indexableOperations, `like`, `ilike`, ] as const /** * All supported operator names in TanStack DB expressions */ export const operators = [ // Comparison operators `eq`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `ilike`, // Logical operators `and`, `or`, `not`, // Null checking `isNull`, `isUndefined`, // String functions `upper`, `lower`, `length`, `concat`, // Numeric functions `add`, `subtract`, `multiply`, `divide`, // Utility functions `coalesce`, `caseWhen`, // Aggregate functions `count`, `avg`, `sum`, `min`, `max`, ] as const export type OperatorName = (typeof operators)[number] export class ToArrayWrapper<_T = unknown> { readonly __brand = `ToArrayWrapper` as const declare readonly _type: `toArray` declare readonly _result: _T constructor( public readonly query: QueryBuilder<any>, public readonly runtime: IncludesRuntime, ) { registerWrapper(this, `toArray()`) } } export class ConcatToArrayWrapper<_T = unknown> { readonly __brand = `ConcatToArrayWrapper` as const declare readonly _type: `concatToArray` declare readonly _result: _T constructor( public readonly query: QueryBuilder<any>, public readonly runtime: IncludesRuntime, ) { registerWrapper(this, `concat(toArray())`) } } export class CaseWhenWrapper<_T = any> { readonly __brand = `CaseWhenWrapper` as const declare readonly _type: `caseWhen` readonly _result?: _T constructor(public readonly args: Array<CaseWhenValue>) { registerWrapper(this, `caseWhen()`) } } export class MaterializeWrapper< _T = unknown, _IsSingle extends boolean = boolean, > { readonly __brand = `MaterializeWrapper` as const declare readonly _type: `materialize` declare readonly _result: _T declare readonly _isSingle: _IsSingle constructor( public readonly query: QueryBuilder<any>, public readonly runtime: IncludesRuntime, ) { registerWrapper(this, `materialize()`) } } function caseWhenHasOnlyExpressionValues(args: Array<CaseWhenValue>): boolean { const valueIndexes = getCaseWhenValueIndexes(args.length) return valueIndexes.every((index) => isExpressionValue(args[index])) } function getCaseWhenValueIndexes(argCount: number): Array<number> { const valueIndexes: Array<number> = [] const hasDefaultValue = argCount % 2 === 1 const pairCount = Math.floor(argCount / 2) for (let i = 0; i < pairCount; i++) { valueIndexes.push(i * 2 + 1) } if (hasDefaultValue) { valueIndexes.push(argCount - 1) } return valueIndexes } function isExpressionValue(value: CaseWhenValue | undefined): boolean { if (isRefProxy(value)) return true if (isBasicOrAggregateExpression(value)) return true if (value == null) return true if ( typeof value === `string` || typeof value === `number` || typeof value === `boolean` || typeof value === `bigint` ) { return true } if (value instanceof Date || Array.isArray(value)) return true return false } function isConditionValue(value: CaseWhenValue | undefined): boolean { return isExpressionValue(value) && !Array.isArray(value) } export class IncludeWrapper<_TContext extends Context = any> { readonly __brand = `IncludeWrapper` as const declare readonly _context: _TContext constructor( public readonly query: BaseQueryBuilder<any>, public readonly runtime: IncludesRuntime, ) { registerWrapper(this, `include()`) } }