UNPKG

@tldraw/store

Version:

tldraw infinite canvas SDK (store).

8 lines (7 loc) 8.09 kB
{ "version": 3, "sources": ["../../src/lib/executeQuery.ts"], "sourcesContent": ["import { IdOf, UnknownRecord } from './BaseRecord'\nimport { intersectSets } from './setUtils'\nimport { StoreQueries } from './StoreQueries'\n\n/**\n * Defines matching criteria for query values. Supports equality, inequality, and greater-than comparisons.\n *\n * @example\n * ```ts\n * // Exact match\n * const exactMatch: QueryValueMatcher<string> = { eq: 'Science Fiction' }\n *\n * // Not equal to\n * const notMatch: QueryValueMatcher<string> = { neq: 'Romance' }\n *\n * // Greater than (numeric values only)\n * const greaterThan: QueryValueMatcher<number> = { gt: 2020 }\n * ```\n *\n * @public\n */\nexport type QueryValueMatcher<T> = { eq: T } | { neq: T } | { gt: number }\n\n/**\n * Query expression for filtering records by their property values. Maps record property names\n * to matching criteria.\n *\n * @example\n * ```ts\n * // Query for books published after 2020 that are in stock\n * const bookQuery: QueryExpression<Book> = {\n * publishedYear: { gt: 2020 },\n * inStock: { eq: true }\n * }\n *\n * // Query for books not by a specific author\n * const notByAuthor: QueryExpression<Book> = {\n * authorId: { neq: 'author:tolkien' }\n * }\n *\n * // Query with nested properties\n * const nestedQuery: QueryExpression<Book> = {\n * metadata: { sessionId: { eq: 'session:alpha' } }\n * }\n * ```\n *\n * @public\n */\n/** @public */\nexport type QueryExpression<R extends object> = {\n\t[k in keyof R & string]?: R[k] extends string | number | boolean | null | undefined\n\t\t? QueryValueMatcher<R[k]>\n\t\t: R[k] extends object\n\t\t\t? QueryExpression<R[k]>\n\t\t\t: QueryValueMatcher<R[k]>\n}\n\nfunction isQueryValueMatcher(value: unknown): value is QueryValueMatcher<unknown> {\n\tif (typeof value !== 'object' || value === null) return false\n\treturn 'eq' in value || 'neq' in value || 'gt' in value\n}\n\nfunction extractMatcherPaths(\n\tquery: QueryExpression<any>,\n\tprefix: string = ''\n): Array<{ path: string; matcher: QueryValueMatcher<any> }> {\n\tconst paths: Array<{ path: string; matcher: QueryValueMatcher<any> }> = []\n\n\tfor (const [key, value] of Object.entries(query)) {\n\t\tconst currentPath = prefix ? `${prefix}\\\\${key}` : key\n\n\t\tif (isQueryValueMatcher(value)) {\n\t\t\t// It's a direct matcher\n\t\t\tpaths.push({ path: currentPath, matcher: value })\n\t\t} else if (typeof value === 'object' && value !== null) {\n\t\t\t// It's a nested query - recurse into it\n\t\t\tpaths.push(...extractMatcherPaths(value as QueryExpression<any>, currentPath))\n\t\t}\n\t}\n\n\treturn paths\n}\n\nexport function objectMatchesQuery<T extends object>(query: QueryExpression<T>, object: T) {\n\tfor (const [key, matcher] of Object.entries(query)) {\n\t\tconst value = object[key as keyof T]\n\n\t\t// if you add matching logic here, make sure you also update executeQuery,\n\t\t// where initial data is pulled out of the indexes, since that requires different\n\t\t// matching logic\n\t\tif (isQueryValueMatcher(matcher)) {\n\t\t\tif ('eq' in matcher && value !== matcher.eq) return false\n\t\t\tif ('neq' in matcher && value === matcher.neq) return false\n\t\t\tif ('gt' in matcher && (typeof value !== 'number' || value <= matcher.gt)) return false\n\t\t\tcontinue\n\t\t}\n\n\t\t// It's a nested query\n\t\tif (typeof value !== 'object' || value === null) return false\n\t\tif (!objectMatchesQuery(matcher as QueryExpression<any>, value as any)) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n/**\n * Executes a query against the store using reactive indexes to efficiently find matching record IDs.\n * Uses the store's internal indexes for optimal performance, especially for equality matches.\n *\n * @param store - The store queries interface providing access to reactive indexes\n * @param typeName - The type name of records to query (e.g., 'book', 'author')\n * @param query - Query expression defining the matching criteria\n * @returns A Set containing the IDs of all records that match the query criteria\n *\n * @example\n * ```ts\n * // Find IDs of all books published after 2020 that are in stock\n * const bookIds = executeQuery(store, 'book', {\n * publishedYear: { gt: 2020 },\n * inStock: { eq: true }\n * })\n *\n * // Find IDs of books not by a specific author\n * const otherBookIds = executeQuery(store, 'book', {\n * authorId: { neq: 'author:tolkien' }\n * })\n *\n * // Query with nested properties\n * const nestedQueryIds = executeQuery(store, 'book', {\n * metadata: { sessionId: { eq: 'session:alpha' } }\n * })\n * ```\n *\n * @public\n */\nexport function executeQuery<R extends UnknownRecord, TypeName extends R['typeName']>(\n\tstore: StoreQueries<R>,\n\ttypeName: TypeName,\n\tquery: QueryExpression<Extract<R, { typeName: TypeName }>>\n): Set<IdOf<Extract<R, { typeName: TypeName }>>> {\n\ttype S = Extract<R, { typeName: TypeName }>\n\n\t// Extract all paths with matchers (flattens nested queries)\n\tconst matcherPaths = extractMatcherPaths(query)\n\n\t// Build a set of matching IDs for each path\n\tconst matchIds = Object.fromEntries(matcherPaths.map(({ path }) => [path, new Set<IdOf<S>>()]))\n\n\t// For each path, use the index to find matching IDs\n\tfor (const { path, matcher } of matcherPaths) {\n\t\tconst index = store.index(typeName, path as any)\n\n\t\tif ('eq' in matcher) {\n\t\t\tconst ids = index.get().get(matcher.eq)\n\t\t\tif (ids) {\n\t\t\t\tfor (const id of ids) {\n\t\t\t\t\tmatchIds[path].add(id)\n\t\t\t\t}\n\t\t\t}\n\t\t} else if ('neq' in matcher) {\n\t\t\tfor (const [value, ids] of index.get()) {\n\t\t\t\tif (value !== matcher.neq) {\n\t\t\t\t\tfor (const id of ids) {\n\t\t\t\t\t\tmatchIds[path].add(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if ('gt' in matcher) {\n\t\t\tfor (const [value, ids] of index.get()) {\n\t\t\t\tif (typeof value === 'number' && value > matcher.gt) {\n\t\t\t\t\tfor (const id of ids) {\n\t\t\t\t\t\tmatchIds[path].add(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Short-circuit if this set is empty - intersection will be empty\n\t\tif (matchIds[path].size === 0) {\n\t\t\treturn new Set()\n\t\t}\n\t}\n\n\t// Intersect all the match sets\n\treturn intersectSets(Object.values(matchIds)) as Set<IdOf<S>>\n}\n"], "mappings": "AACA,SAAS,qBAAqB;AAwD9B,SAAS,oBAAoB,OAAqD;AACjF,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,SAAO,QAAQ,SAAS,SAAS,SAAS,QAAQ;AACnD;AAEA,SAAS,oBACR,OACA,SAAiB,IAC0C;AAC3D,QAAM,QAAkE,CAAC;AAEzE,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,UAAM,cAAc,SAAS,GAAG,MAAM,KAAK,GAAG,KAAK;AAEnD,QAAI,oBAAoB,KAAK,GAAG;AAE/B,YAAM,KAAK,EAAE,MAAM,aAAa,SAAS,MAAM,CAAC;AAAA,IACjD,WAAW,OAAO,UAAU,YAAY,UAAU,MAAM;AAEvD,YAAM,KAAK,GAAG,oBAAoB,OAA+B,WAAW,CAAC;AAAA,IAC9E;AAAA,EACD;AAEA,SAAO;AACR;AAEO,SAAS,mBAAqC,OAA2B,QAAW;AAC1F,aAAW,CAAC,KAAK,OAAO,KAAK,OAAO,QAAQ,KAAK,GAAG;AACnD,UAAM,QAAQ,OAAO,GAAc;AAKnC,QAAI,oBAAoB,OAAO,GAAG;AACjC,UAAI,QAAQ,WAAW,UAAU,QAAQ,GAAI,QAAO;AACpD,UAAI,SAAS,WAAW,UAAU,QAAQ,IAAK,QAAO;AACtD,UAAI,QAAQ,YAAY,OAAO,UAAU,YAAY,SAAS,QAAQ,IAAK,QAAO;AAClF;AAAA,IACD;AAGA,QAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAI,CAAC,mBAAmB,SAAiC,KAAY,GAAG;AACvE,aAAO;AAAA,IACR;AAAA,EACD;AACA,SAAO;AACR;AAgCO,SAAS,aACf,OACA,UACA,OACgD;AAIhD,QAAM,eAAe,oBAAoB,KAAK;AAG9C,QAAM,WAAW,OAAO,YAAY,aAAa,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,MAAM,oBAAI,IAAa,CAAC,CAAC,CAAC;AAG9F,aAAW,EAAE,MAAM,QAAQ,KAAK,cAAc;AAC7C,UAAM,QAAQ,MAAM,MAAM,UAAU,IAAW;AAE/C,QAAI,QAAQ,SAAS;AACpB,YAAM,MAAM,MAAM,IAAI,EAAE,IAAI,QAAQ,EAAE;AACtC,UAAI,KAAK;AACR,mBAAW,MAAM,KAAK;AACrB,mBAAS,IAAI,EAAE,IAAI,EAAE;AAAA,QACtB;AAAA,MACD;AAAA,IACD,WAAW,SAAS,SAAS;AAC5B,iBAAW,CAAC,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG;AACvC,YAAI,UAAU,QAAQ,KAAK;AAC1B,qBAAW,MAAM,KAAK;AACrB,qBAAS,IAAI,EAAE,IAAI,EAAE;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAAA,IACD,WAAW,QAAQ,SAAS;AAC3B,iBAAW,CAAC,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG;AACvC,YAAI,OAAO,UAAU,YAAY,QAAQ,QAAQ,IAAI;AACpD,qBAAW,MAAM,KAAK;AACrB,qBAAS,IAAI,EAAE,IAAI,EAAE;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAGA,QAAI,SAAS,IAAI,EAAE,SAAS,GAAG;AAC9B,aAAO,oBAAI,IAAI;AAAA,IAChB;AAAA,EACD;AAGA,SAAO,cAAc,OAAO,OAAO,QAAQ,CAAC;AAC7C;", "names": [] }