@tanstack/db
Version:
A reactive client store for building super fast apps on sync
1 lines • 20 kB
Source Map (JSON)
{"version":3,"file":"expression-helpers.cjs","sources":["../../../src/query/expression-helpers.ts"],"sourcesContent":["/**\n * Expression Helpers for TanStack DB\n *\n * These utilities help parse LoadSubsetOptions (where, orderBy, limit) from TanStack DB\n * into formats suitable for your API backend. They provide a generic way to traverse\n * expression trees without having to implement your own parser.\n *\n * @example\n * ```typescript\n * import { parseWhereExpression, parseOrderByExpression } from '@tanstack/db'\n *\n * queryFn: async (ctx) => {\n * const { limit, where, orderBy } = ctx.meta?.loadSubsetOptions ?? {}\n *\n * // Convert expression tree to filters\n * const filters = parseWhereExpression(where, {\n * eq: (field, value) => ({ [field]: value }),\n * lt: (field, value) => ({ [`${field}_lt`]: value }),\n * and: (filters) => Object.assign({}, ...filters)\n * })\n *\n * // Extract sort information\n * const sort = parseOrderByExpression(orderBy)\n *\n * return api.getProducts({ ...filters, sort, limit })\n * }\n * ```\n */\n\nimport type { IR, OperatorName } from '../index.js'\n\ntype BasicExpression<T = any> = IR.BasicExpression<T>\ntype OrderBy = IR.OrderBy\n\n/**\n * Represents a simple field path extracted from an expression.\n * Can include string keys for object properties and numbers for array indices.\n */\nexport type FieldPath = Array<string | number>\n\n/**\n * Represents a simple comparison operation\n */\nexport interface SimpleComparison {\n field: FieldPath\n operator: string\n value?: any // Optional for operators like isNull and isUndefined that don't have a value\n}\n\n/**\n * Options for customizing how WHERE expressions are parsed\n */\nexport interface ParseWhereOptions<T = any> {\n /**\n * Handler functions for different operators.\n * Each handler receives the parsed field path(s) and value(s) and returns your custom format.\n *\n * Supported operators from TanStack DB:\n * - Comparison: eq, gt, gte, lt, lte, in, like, ilike\n * - Logical: and, or, not\n * - Null checking: isNull, isUndefined\n * - String functions: upper, lower, length, concat\n * - Numeric: add\n * - Utility: coalesce\n * - Aggregates: count, avg, sum, min, max\n */\n handlers: {\n [K in OperatorName]?: (...args: Array<any>) => T\n } & {\n [key: string]: (...args: Array<any>) => T\n }\n /**\n * Optional handler for when an unknown operator is encountered.\n * If not provided, unknown operators throw an error.\n */\n onUnknownOperator?: (operator: string, args: Array<any>) => T\n}\n\n/**\n * Result of parsing an ORDER BY expression\n */\nexport interface ParsedOrderBy {\n field: FieldPath\n direction: `asc` | `desc`\n nulls: `first` | `last`\n /** String sorting method: 'lexical' (default) or 'locale' (locale-aware) */\n stringSort?: `lexical` | `locale`\n /** Locale for locale-aware string sorting (e.g., 'en-US') */\n locale?: string\n /** Additional options for locale-aware sorting */\n localeOptions?: object\n}\n\n/**\n * Extracts the field path from a PropRef expression.\n * Returns null for non-ref expressions.\n *\n * @param expr - The expression to extract from\n * @returns The field path array, or null\n *\n * @example\n * ```typescript\n * const field = extractFieldPath(someExpression)\n * // Returns: ['product', 'category']\n * ```\n */\nexport function extractFieldPath(expr: BasicExpression): FieldPath | null {\n if (expr.type === `ref`) {\n return expr.path\n }\n return null\n}\n\n/**\n * Extracts the value from a Value expression.\n * Returns undefined for non-value expressions.\n *\n * @param expr - The expression to extract from\n * @returns The extracted value\n *\n * @example\n * ```typescript\n * const val = extractValue(someExpression)\n * // Returns: 'electronics'\n * ```\n */\nexport function extractValue(expr: BasicExpression): any {\n if (expr.type === `val`) {\n return expr.value\n }\n return undefined\n}\n\n/**\n * Generic expression tree walker that visits each node in the expression.\n * Useful for implementing custom parsing logic.\n *\n * @param expr - The expression to walk\n * @param visitor - Visitor function called for each node\n *\n * @example\n * ```typescript\n * walkExpression(whereExpr, (node) => {\n * if (node.type === 'func' && node.name === 'eq') {\n * console.log('Found equality comparison')\n * }\n * })\n * ```\n */\nexport function walkExpression(\n expr: BasicExpression | undefined | null,\n visitor: (node: BasicExpression) => void,\n): void {\n if (!expr) return\n\n visitor(expr)\n\n if (expr.type === `func`) {\n expr.args.forEach((arg: BasicExpression) => walkExpression(arg, visitor))\n }\n}\n\n/**\n * Parses a WHERE expression into a custom format using provided handlers.\n *\n * This is the main helper for converting TanStack DB where clauses into your API's filter format.\n * You provide handlers for each operator, and this function traverses the expression tree\n * and calls the appropriate handlers.\n *\n * @param expr - The WHERE expression to parse\n * @param options - Configuration with handler functions for each operator\n * @returns The parsed result in your custom format\n *\n * @example\n * ```typescript\n * // REST API with query parameters\n * const filters = parseWhereExpression(where, {\n * handlers: {\n * eq: (field, value) => ({ [field.join('.')]: value }),\n * lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),\n * gt: (field, value) => ({ [`${field.join('.')}_gt`]: value }),\n * and: (...filters) => Object.assign({}, ...filters),\n * or: (...filters) => ({ $or: filters })\n * }\n * })\n * // Returns: { category: 'electronics', price_lt: 100 }\n * ```\n *\n * @example\n * ```typescript\n * // GraphQL where clause\n * const where = parseWhereExpression(whereExpr, {\n * handlers: {\n * eq: (field, value) => ({ [field.join('_')]: { _eq: value } }),\n * lt: (field, value) => ({ [field.join('_')]: { _lt: value } }),\n * and: (...filters) => ({ _and: filters })\n * }\n * })\n * ```\n */\nexport function parseWhereExpression<T = any>(\n expr: BasicExpression<boolean> | undefined | null,\n options: ParseWhereOptions<T>,\n): T | null {\n if (!expr) return null\n\n const { handlers, onUnknownOperator } = options\n\n // Handle value expressions\n if (expr.type === `val`) {\n return expr.value as unknown as T\n }\n\n // Handle property references\n if (expr.type === `ref`) {\n return expr.path as unknown as T\n }\n\n // Handle function expressions\n // After checking val and ref, expr must be func\n const { name, args } = expr\n const handler = handlers[name]\n\n if (!handler) {\n if (onUnknownOperator) {\n return onUnknownOperator(name, args)\n }\n throw new Error(\n `No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`,\n )\n }\n\n // Parse arguments recursively\n const parsedArgs = args.map((arg: BasicExpression) => {\n // For refs, extract the field path\n if (arg.type === `ref`) {\n return arg.path\n }\n // For values, extract the value\n if (arg.type === `val`) {\n return arg.value\n }\n // For nested functions, recurse (after checking ref and val, must be func)\n return parseWhereExpression(arg, options)\n })\n\n return handler(...parsedArgs)\n}\n\n/**\n * Parses an ORDER BY expression into a simple array of sort specifications.\n *\n * @param orderBy - The ORDER BY expression array\n * @returns Array of parsed order by specifications\n *\n * @example\n * ```typescript\n * const sorts = parseOrderByExpression(orderBy)\n * // Returns: [\n * // { field: ['category'], direction: 'asc', nulls: 'last' },\n * // { field: ['price'], direction: 'desc', nulls: 'last' }\n * // ]\n * ```\n */\nexport function parseOrderByExpression(\n orderBy: OrderBy | undefined | null,\n): Array<ParsedOrderBy> {\n if (!orderBy || orderBy.length === 0) {\n return []\n }\n\n return orderBy.map((clause: IR.OrderByClause) => {\n const field = extractFieldPath(clause.expression)\n\n if (!field) {\n throw new Error(\n `ORDER BY expression must be a field reference, got: ${clause.expression.type}`,\n )\n }\n\n const { direction, nulls } = clause.compareOptions\n const result: ParsedOrderBy = {\n field,\n direction,\n nulls,\n }\n\n // Add string collation options if present (discriminated union)\n if (`stringSort` in clause.compareOptions) {\n result.stringSort = clause.compareOptions.stringSort\n }\n if (`locale` in clause.compareOptions) {\n result.locale = clause.compareOptions.locale\n }\n if (`localeOptions` in clause.compareOptions) {\n result.localeOptions = clause.compareOptions.localeOptions\n }\n\n return result\n })\n}\n\n/**\n * Extracts all simple comparisons from a WHERE expression.\n * This is useful for simple APIs that only support basic filters.\n *\n * Note: This only works for simple AND-ed conditions and NOT-wrapped comparisons.\n * Throws an error if it encounters unsupported operations like OR or complex nested expressions.\n *\n * NOT operators are flattened by prefixing the operator name (e.g., `not(eq(...))` becomes `not_eq`).\n *\n * @param expr - The WHERE expression to parse\n * @returns Array of simple comparisons\n * @throws Error if expression contains OR or other unsupported operations\n *\n * @example\n * ```typescript\n * const comparisons = extractSimpleComparisons(where)\n * // Returns: [\n * // { field: ['category'], operator: 'eq', value: 'electronics' },\n * // { field: ['price'], operator: 'lt', value: 100 },\n * // { field: ['email'], operator: 'isNull' }, // No value for null checks\n * // { field: ['status'], operator: 'not_eq', value: 'archived' }\n * // ]\n * ```\n */\nexport function extractSimpleComparisons(\n expr: BasicExpression<boolean> | undefined | null,\n): Array<SimpleComparison> {\n if (!expr) return []\n\n const comparisons: Array<SimpleComparison> = []\n\n function extract(e: BasicExpression): void {\n if (e.type === `func`) {\n // Handle AND - recurse into both sides\n if (e.name === `and`) {\n e.args.forEach((arg: BasicExpression) => extract(arg))\n return\n }\n\n // Handle NOT - recurse into argument and prefix operator with 'not_'\n if (e.name === `not`) {\n const [arg] = e.args\n if (!arg || arg.type !== `func`) {\n throw new Error(\n `extractSimpleComparisons requires a comparison or null check inside 'not' operator.`,\n )\n }\n\n // Handle NOT with null/undefined checks\n const nullCheckOps = [`isNull`, `isUndefined`]\n if (nullCheckOps.includes(arg.name)) {\n const [fieldArg] = arg.args\n const field = fieldArg?.type === `ref` ? fieldArg.path : null\n\n if (field) {\n comparisons.push({\n field,\n operator: `not_${arg.name}`,\n // No value for null/undefined checks\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires a field reference for '${arg.name}' operator.`,\n )\n }\n return\n }\n\n // Handle NOT with comparison operators\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (comparisonOps.includes(arg.name)) {\n const [leftArg, rightArg] = arg.args\n const field = leftArg?.type === `ref` ? leftArg.path : null\n const value = rightArg?.type === `val` ? rightArg.value : null\n\n if (field && value !== undefined) {\n comparisons.push({\n field,\n operator: `not_${arg.name}`,\n value,\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`,\n )\n }\n return\n }\n\n // NOT can only wrap simple comparisons or null checks\n throw new Error(\n `extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`,\n )\n }\n\n // Throw on unsupported operations\n const unsupportedOps = [\n `or`,\n `like`,\n `ilike`,\n `upper`,\n `lower`,\n `length`,\n `concat`,\n `add`,\n `coalesce`,\n `count`,\n `avg`,\n `sum`,\n `min`,\n `max`,\n ]\n if (unsupportedOps.includes(e.name)) {\n throw new Error(\n `extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`,\n )\n }\n\n // Handle null/undefined check operators (single argument, no value)\n const nullCheckOps = [`isNull`, `isUndefined`]\n if (nullCheckOps.includes(e.name)) {\n const [fieldArg] = e.args\n\n // Extract field (must be a ref)\n const field = fieldArg?.type === `ref` ? fieldArg.path : null\n\n if (field) {\n comparisons.push({\n field,\n operator: e.name,\n // No value for null/undefined checks\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires a field reference for '${e.name}' operator.`,\n )\n }\n return\n }\n\n // Handle comparison operators\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (comparisonOps.includes(e.name)) {\n const [leftArg, rightArg] = e.args\n\n // Extract field and value\n const field = leftArg?.type === `ref` ? leftArg.path : null\n const value = rightArg?.type === `val` ? rightArg.value : null\n\n if (field && value !== undefined) {\n comparisons.push({\n field,\n operator: e.name,\n value,\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`,\n )\n }\n } else {\n // Unknown operator\n throw new Error(\n `extractSimpleComparisons encountered unknown operator: '${e.name}'`,\n )\n }\n }\n }\n\n extract(expr)\n return comparisons\n}\n\n/**\n * Convenience function to get all LoadSubsetOptions in a pre-parsed format.\n * Good starting point for simple use cases.\n *\n * @param options - The LoadSubsetOptions from ctx.meta\n * @returns Pre-parsed filters, sorts, and limit\n *\n * @example\n * ```typescript\n * queryFn: async (ctx) => {\n * const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)\n *\n * // Convert to your API format\n * return api.getProducts({\n * ...Object.fromEntries(\n * parsed.filters.map(f => [`${f.field.join('.')}_${f.operator}`, f.value])\n * ),\n * sort: parsed.sorts.map(s => `${s.field.join('.')}:${s.direction}`).join(','),\n * limit: parsed.limit\n * })\n * }\n * ```\n */\nexport function parseLoadSubsetOptions(\n options:\n | {\n where?: BasicExpression<boolean>\n orderBy?: OrderBy\n limit?: number\n }\n | undefined\n | null,\n): {\n filters: Array<SimpleComparison>\n sorts: Array<ParsedOrderBy>\n limit?: number\n} {\n if (!options) {\n return { filters: [], sorts: [] }\n }\n\n return {\n filters: extractSimpleComparisons(options.where),\n sorts: parseOrderByExpression(options.orderBy),\n limit: options.limit,\n }\n}\n"],"names":["nullCheckOps","comparisonOps"],"mappings":"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"}