snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
192
packages/store/src/lib/executeQuery.ts
Normal file
192
packages/store/src/lib/executeQuery.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { objectMapFromEntries, objectMapValues } from '@tldraw/utils'
|
||||
import { IdOf, UnknownRecord } from './BaseRecord'
|
||||
import { intersectSets } from './setUtils'
|
||||
import { StoreQueries } from './StoreQueries'
|
||||
|
||||
/**
|
||||
* Defines matching criteria for query values. Supports equality, inequality, and greater-than comparisons.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Exact match
|
||||
* const exactMatch: QueryValueMatcher<string> = { eq: 'Science Fiction' }
|
||||
*
|
||||
* // Not equal to
|
||||
* const notMatch: QueryValueMatcher<string> = { neq: 'Romance' }
|
||||
*
|
||||
* // Greater than (numeric values only)
|
||||
* const greaterThan: QueryValueMatcher<number> = { gt: 2020 }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type QueryValueMatcher<T> = { eq: T } | { neq: T } | { gt: number }
|
||||
|
||||
/**
|
||||
* Query expression for filtering records by their property values. Maps record property names
|
||||
* to matching criteria.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Query for books published after 2020 that are in stock
|
||||
* const bookQuery: QueryExpression<Book> = {
|
||||
* publishedYear: { gt: 2020 },
|
||||
* inStock: { eq: true }
|
||||
* }
|
||||
*
|
||||
* // Query for books not by a specific author
|
||||
* const notByAuthor: QueryExpression<Book> = {
|
||||
* authorId: { neq: 'author:tolkien' }
|
||||
* }
|
||||
*
|
||||
* // Query with nested properties
|
||||
* const nestedQuery: QueryExpression<Book> = {
|
||||
* metadata: { sessionId: { eq: 'session:alpha' } }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
/** @public */
|
||||
export type QueryExpression<R extends object> = {
|
||||
[k in keyof R & string]?: R[k] extends string | number | boolean | null | undefined
|
||||
? QueryValueMatcher<R[k]>
|
||||
: R[k] extends object
|
||||
? QueryExpression<R[k]>
|
||||
: QueryValueMatcher<R[k]>
|
||||
}
|
||||
|
||||
function isQueryValueMatcher(value: unknown): value is QueryValueMatcher<unknown> {
|
||||
if (typeof value !== 'object' || value === null) return false
|
||||
return 'eq' in value || 'neq' in value || 'gt' in value
|
||||
}
|
||||
|
||||
function extractMatcherPaths(
|
||||
query: QueryExpression<any>,
|
||||
prefix: string = ''
|
||||
): Array<{ path: string; matcher: QueryValueMatcher<any> }> {
|
||||
const paths: Array<{ path: string; matcher: QueryValueMatcher<any> }> = []
|
||||
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
const currentPath = prefix ? `${prefix}\\${key}` : key
|
||||
|
||||
if (isQueryValueMatcher(value)) {
|
||||
// It's a direct matcher
|
||||
paths.push({ path: currentPath, matcher: value })
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// It's a nested query - recurse into it
|
||||
paths.push(...extractMatcherPaths(value as QueryExpression<any>, currentPath))
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export function objectMatchesQuery<T extends object>(query: QueryExpression<T>, object: T) {
|
||||
for (const [key, matcher] of Object.entries(query)) {
|
||||
const value = object[key as keyof T]
|
||||
|
||||
// if you add matching logic here, make sure you also update executeQuery,
|
||||
// where initial data is pulled out of the indexes, since that requires different
|
||||
// matching logic
|
||||
if (isQueryValueMatcher(matcher)) {
|
||||
if ('eq' in matcher && value !== matcher.eq) return false
|
||||
// undefined values must not match neq: the indexes executeQuery reads only
|
||||
// track defined values, and the two matching strategies have to agree
|
||||
if ('neq' in matcher && (value === matcher.neq || value === undefined)) return false
|
||||
if ('gt' in matcher && (typeof value !== 'number' || value <= matcher.gt)) return false
|
||||
continue
|
||||
}
|
||||
|
||||
// It's a nested query
|
||||
if (typeof value !== 'object' || value === null) return false
|
||||
if (!objectMatchesQuery(matcher as QueryExpression<any>, value as any)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query against the store using reactive indexes to efficiently find matching record IDs.
|
||||
* Uses the store's internal indexes for optimal performance, especially for equality matches.
|
||||
*
|
||||
* @param store - The store queries interface providing access to reactive indexes
|
||||
* @param typeName - The type name of records to query (e.g., 'book', 'author')
|
||||
* @param query - Query expression defining the matching criteria
|
||||
* @returns A Set containing the IDs of all records that match the query criteria
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Find IDs of all books published after 2020 that are in stock
|
||||
* const bookIds = executeQuery(store, 'book', {
|
||||
* publishedYear: { gt: 2020 },
|
||||
* inStock: { eq: true }
|
||||
* })
|
||||
*
|
||||
* // Find IDs of books not by a specific author
|
||||
* const otherBookIds = executeQuery(store, 'book', {
|
||||
* authorId: { neq: 'author:tolkien' }
|
||||
* })
|
||||
*
|
||||
* // Query with nested properties
|
||||
* const nestedQueryIds = executeQuery(store, 'book', {
|
||||
* metadata: { sessionId: { eq: 'session:alpha' } }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function executeQuery<R extends UnknownRecord, TypeName extends R['typeName']>(
|
||||
store: StoreQueries<R>,
|
||||
typeName: TypeName,
|
||||
query: QueryExpression<Extract<R, { typeName: TypeName }>>
|
||||
): Set<IdOf<Extract<R, { typeName: TypeName }>>> {
|
||||
type S = Extract<R, { typeName: TypeName }>
|
||||
|
||||
// Extract all paths with matchers (flattens nested queries)
|
||||
const matcherPaths = extractMatcherPaths(query)
|
||||
|
||||
// Build a set of matching IDs for each path
|
||||
const matchIds = objectMapFromEntries(
|
||||
matcherPaths.map(({ path }) => [path, new Set<IdOf<S>>()] as const)
|
||||
)
|
||||
|
||||
// For each path, use the index to find matching IDs
|
||||
for (const { path, matcher } of matcherPaths) {
|
||||
const index = store.index(typeName, path as any)
|
||||
|
||||
if ('eq' in matcher) {
|
||||
const ids = index.get().get(matcher.eq)
|
||||
if (ids) {
|
||||
for (const id of ids) {
|
||||
matchIds[path].add(id)
|
||||
}
|
||||
}
|
||||
} else if ('neq' in matcher) {
|
||||
for (const [value, ids] of index.get()) {
|
||||
if (value !== matcher.neq) {
|
||||
for (const id of ids) {
|
||||
matchIds[path].add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ('gt' in matcher) {
|
||||
for (const [value, ids] of index.get()) {
|
||||
if (typeof value === 'number' && value > matcher.gt) {
|
||||
for (const id of ids) {
|
||||
matchIds[path].add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Short-circuit if this set is empty - intersection will be empty
|
||||
if (matchIds[path].size === 0) {
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
// Intersect all the match sets
|
||||
return intersectSets(objectMapValues(matchIds)) as Set<IdOf<S>>
|
||||
}
|
||||
Reference in New Issue
Block a user