UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

397 lines (351 loc) • 12.3 kB
import { PropRef, Value, isBasicOrAggregateExpression } from '../ir.js' import { isRefProxy, registerRefProxy } from './ref-proxy-identity.js' import { getWrapperExpressionName } from './wrapper-identity.js' import type { BasicExpression } from '../ir.js' import type { IsPlainObject, RefLeaf } from './types.js' import type { VirtualRowProps } from '../../virtual-props.js' export { isRefProxy } from './ref-proxy-identity.js' export interface RefProxy<T = any> { /** @internal */ readonly __refProxy: true /** @internal */ readonly __path: Array<string> /** @internal */ readonly __sourceAlias?: string /** @internal */ readonly __type: T } /** * Virtual properties available on all row ref proxies. * These allow querying on sync status, origin, key, and collection ID. */ export type VirtualPropsRefProxy< TKey extends string | number = string | number, > = { readonly [K in keyof VirtualRowProps<TKey>]: RefLeaf<VirtualRowProps<TKey>[K]> } // Strip nullish members before deciding whether a schema field is traversable, // then restore them outside the proxy so the schema still requires a guard. // The tuple guard keeps exact null/undefined fields as leaves: `never` would // otherwise satisfy the plain-object check. type SingleRowField<V, TKey extends string | number> = [ NonNullable<V>, ] extends [never] ? RefLeaf<V> : IsPlainObject<NonNullable<V>> extends true ? | SingleRowRefProxy<NonNullable<V>, TKey, false> | Extract<V, null | undefined> : RefLeaf<V> /** * Type for creating a RefProxy for a single row/type without namespacing * Used in collection indexes and where clauses * * Inferred row roots include virtual properties ($synced, $origin, $key, * $collectionId). The default exported shape is suitable for reusable helpers * that can accept either roots or recursively traversed user objects. Use the * third parameter as `true` when a helper specifically requires a row root. */ export type SingleRowRefProxy< T, TKey extends string | number = string | number, IncludeVirtualProps extends boolean = false, > = T extends Record<string, any> ? { [K in keyof T]: SingleRowField<T[K], TKey> } & RefProxy<T> & (IncludeVirtualProps extends true ? VirtualPropsRefProxy<TKey> : {}) : RefProxy<T> & (IncludeVirtualProps extends true ? VirtualPropsRefProxy<TKey> : {}) /** * Creates a proxy object that records property access paths for a single row * Used in collection indexes and where clauses */ export function createSingleRowRefProxy< T extends Record<string, any>, TKey extends string | number = string | number, >(): SingleRowRefProxy<T, TKey, true> { const cache = new Map<string, any>() function createProxy(path: Array<string>): any { const pathKey = JSON.stringify(path) if (cache.has(pathKey)) { return cache.get(pathKey) } const proxy = new Proxy({} as any, { get(target, prop, receiver) { if (prop === `__refProxy`) return true if (prop === `__path`) return path if (prop === `__sourceAlias`) return undefined if (prop === `__type`) return undefined // Type is only for TypeScript inference if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const newPath = [...path, String(prop)] return createProxy(newPath) }, has(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) return true return Reflect.has(target, prop) }, ownKeys(target) { return Reflect.ownKeys(target) }, getOwnPropertyDescriptor(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) { return { enumerable: false, configurable: true } } return Reflect.getOwnPropertyDescriptor(target, prop) }, }) registerRefProxy(proxy) cache.set(pathKey, proxy) return proxy } // Return the root proxy that starts with an empty path return createProxy([]) as SingleRowRefProxy<T, TKey, true> } /** * Creates a proxy object that records property access paths * Used in callbacks like where, select, etc. to create type-safe references */ export function createRefProxy<T extends Record<string, any>>( aliases: Array<string>, ): RefProxy<T> & T { const cache = new Map<string, any>() let accessId = 0 // Monotonic counter to record evaluation order function createProxy(path: Array<string>): any { const pathKey = JSON.stringify(path) if (cache.has(pathKey)) { return cache.get(pathKey) } const proxy = new Proxy({} as any, { get(target, prop, receiver) { if (prop === `__refProxy`) return true if (prop === `__path`) return path if (prop === `__sourceAlias`) return path[0] if (prop === `__type`) return undefined // Type is only for TypeScript inference if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const newPath = [...path, String(prop)] return createProxy(newPath) }, has(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) return true return Reflect.has(target, prop) }, ownKeys(target) { const id = ++accessId const sentinelKey = `__SPREAD_SENTINEL__${path.join(`.`)}__${id}` if (!Object.prototype.hasOwnProperty.call(target, sentinelKey)) { Object.defineProperty(target, sentinelKey, { enumerable: true, configurable: true, value: true, }) } return Reflect.ownKeys(target) }, getOwnPropertyDescriptor(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) { return { enumerable: false, configurable: true } } return Reflect.getOwnPropertyDescriptor(target, prop) }, }) registerRefProxy(proxy) cache.set(pathKey, proxy) return proxy } // Create the root proxy with all aliases as top-level properties const rootProxy = new Proxy({} as any, { get(target, prop, receiver) { if (prop === `__refProxy`) return true if (prop === `__path`) return [] if (prop === `__sourceAlias`) return undefined if (prop === `__type`) return undefined // Type is only for TypeScript inference if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const propStr = String(prop) if (aliases.includes(propStr) || aliases.includes(`*`)) { return createProxy([propStr]) } return undefined }, has(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) return true if (typeof prop === `string` && aliases.includes(prop)) return true return Reflect.has(target, prop) }, ownKeys(_target) { return [...aliases, `__refProxy`, `__path`, `__sourceAlias`, `__type`] }, getOwnPropertyDescriptor(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) { return { enumerable: false, configurable: true } } if (typeof prop === `string` && aliases.includes(prop)) { return { enumerable: true, configurable: true } } return undefined }, }) registerRefProxy(rootProxy) return rootProxy } /** * Creates a ref proxy with $selected namespace for SELECT fields * * Adds a $selected property that allows accessing SELECT fields via $selected.fieldName syntax. * The $selected proxy creates paths like ['$selected', 'fieldName'] which directly reference * the $selected property on the namespaced row. * * @param aliases - Array of table aliases to create proxies for * @returns A ref proxy with table aliases and $selected namespace */ export function createRefProxyWithSelected<T extends Record<string, any>>( aliases: Array<string>, ): RefProxy<T> & T & { $selected: SingleRowRefProxy<any, string | number, true> } { const baseProxy = createRefProxy(aliases) // Create a proxy for $selected that prefixes all paths with '$selected' const cache = new Map<string, any>() function createSelectedProxy(path: Array<string>): any { const pathKey = JSON.stringify(path) if (cache.has(pathKey)) { return cache.get(pathKey) } const proxy = new Proxy({} as any, { get(target, prop, receiver) { if (prop === `__refProxy`) return true if (prop === `__path`) return [`$selected`, ...path] if (prop === `__sourceAlias`) return `$selected` if (prop === `__type`) return undefined if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const newPath = [...path, String(prop)] return createSelectedProxy(newPath) }, has(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) return true return Reflect.has(target, prop) }, ownKeys(target) { return Reflect.ownKeys(target) }, getOwnPropertyDescriptor(target, prop) { if ( prop === `__refProxy` || prop === `__path` || prop === `__sourceAlias` || prop === `__type` ) { return { enumerable: false, configurable: true } } return Reflect.getOwnPropertyDescriptor(target, prop) }, }) registerRefProxy(proxy) cache.set(pathKey, proxy) return proxy } const wrappedSelectedProxy = createSelectedProxy([]) // Wrap the base proxy to also handle $selected access const selectedRootProxy = new Proxy(baseProxy, { get(target, prop, receiver) { if (prop === `$selected`) { return wrappedSelectedProxy } return Reflect.get(target, prop, receiver) }, has(target, prop) { if (prop === `$selected`) return true return Reflect.has(target, prop) }, ownKeys(target) { return [...Reflect.ownKeys(target), `$selected`] }, getOwnPropertyDescriptor(target, prop) { if (prop === `$selected`) { return { enumerable: true, configurable: true, value: wrappedSelectedProxy, } } return Reflect.getOwnPropertyDescriptor(target, prop) }, }) as RefProxy<T> & T & { $selected: SingleRowRefProxy<any, string | number, true> } registerRefProxy(selectedRootProxy) return selectedRootProxy } /** * Converts a value to an Expression. * If it's a RefProxy, creates a PropRef. Throws if the value is a * ToArrayWrapper, ConcatToArrayWrapper, CaseWhenWrapper, or MaterializeWrapper * (these must be used as direct select fields). Otherwise wraps it as a Value. */ export function toExpression<T = any>(value: T): BasicExpression<T> export function toExpression(value: RefProxy<any>): BasicExpression<any> export function toExpression(value: any): BasicExpression<any> { if (isRefProxy(value)) { return new PropRef(value.__path, value.__sourceAlias) } // toArray(), concat(toArray()), and materialize() must be used as direct // select fields, not inside expressions const name = getWrapperExpressionName(value) if (name) { throw new Error( `${name} cannot be used inside expressions (e.g., coalesce(), eq(), not()). ` + `Use ${name} directly as a select field value instead.`, ) } // Only constructed expressions are IR; user values may have the same fields. if (isBasicOrAggregateExpression(value)) { return value as BasicExpression } return new Value(value) } /** * Helper to create a Value expression from a literal */ export function val<T>(value: T): BasicExpression<T> { return new Value(value) }