@tldraw/store 4.2.0-next.d76c345101d5 → 4.2.0-next.de0584cc1c90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +21 -12
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/StoreQueries.js +73 -27
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js +38 -14
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-esm/index.d.mts +21 -12
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/StoreQueries.mjs +73 -27
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs +38 -14
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/StoreQueries.ts +102 -51
- package/src/lib/executeQuery.test.ts +928 -4
- package/src/lib/executeQuery.ts +78 -36
package/src/lib/executeQuery.ts
CHANGED
|
@@ -37,44 +37,69 @@ export type QueryValueMatcher<T> = { eq: T } | { neq: T } | { gt: number }
|
|
|
37
37
|
* const notByAuthor: QueryExpression<Book> = {
|
|
38
38
|
* authorId: { neq: 'author:tolkien' }
|
|
39
39
|
* }
|
|
40
|
+
*
|
|
41
|
+
* // Query with nested properties
|
|
42
|
+
* const nestedQuery: QueryExpression<Book> = {
|
|
43
|
+
* metadata: { sessionId: { eq: 'session:alpha' } }
|
|
44
|
+
* }
|
|
40
45
|
* ```
|
|
41
46
|
*
|
|
42
47
|
* @public
|
|
43
48
|
*/
|
|
49
|
+
/** @public */
|
|
44
50
|
export type QueryExpression<R extends object> = {
|
|
45
|
-
[k in keyof R & string]?:
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
[k in keyof R & string]?: R[k] extends string | number | boolean | null | undefined
|
|
52
|
+
? QueryValueMatcher<R[k]>
|
|
53
|
+
: R[k] extends object
|
|
54
|
+
? QueryExpression<R[k]>
|
|
55
|
+
: QueryValueMatcher<R[k]>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isQueryValueMatcher(value: unknown): value is QueryValueMatcher<unknown> {
|
|
59
|
+
if (typeof value !== 'object' || value === null) return false
|
|
60
|
+
return 'eq' in value || 'neq' in value || 'gt' in value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractMatcherPaths(
|
|
64
|
+
query: QueryExpression<any>,
|
|
65
|
+
prefix: string = ''
|
|
66
|
+
): Array<{ path: string; matcher: QueryValueMatcher<any> }> {
|
|
67
|
+
const paths: Array<{ path: string; matcher: QueryValueMatcher<any> }> = []
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of Object.entries(query)) {
|
|
70
|
+
const currentPath = prefix ? `${prefix}\\${key}` : key
|
|
71
|
+
|
|
72
|
+
if (isQueryValueMatcher(value)) {
|
|
73
|
+
// It's a direct matcher
|
|
74
|
+
paths.push({ path: currentPath, matcher: value })
|
|
75
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
76
|
+
// It's a nested query - recurse into it
|
|
77
|
+
paths.push(...extractMatcherPaths(value as QueryExpression<any>, currentPath))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return paths
|
|
48
82
|
}
|
|
49
83
|
|
|
50
|
-
/**
|
|
51
|
-
* Tests whether an object matches the given query expression by checking each property
|
|
52
|
-
* against its corresponding matcher criteria.
|
|
53
|
-
*
|
|
54
|
-
* @param query - The query expression containing matching criteria for object properties
|
|
55
|
-
* @param object - The object to test against the query
|
|
56
|
-
* @returns True if the object matches all criteria in the query, false otherwise
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* const book = { title: '1984', publishedYear: 1949, inStock: true }
|
|
61
|
-
* const query = { publishedYear: { gt: 1945 }, inStock: { eq: true } }
|
|
62
|
-
*
|
|
63
|
-
* const matches = objectMatchesQuery(query, book) // true
|
|
64
|
-
* ```
|
|
65
|
-
*
|
|
66
|
-
* @public
|
|
67
|
-
*/
|
|
68
84
|
export function objectMatchesQuery<T extends object>(query: QueryExpression<T>, object: T) {
|
|
69
|
-
for (const [key,
|
|
70
|
-
const matcher = _matcher as QueryValueMatcher<T>
|
|
85
|
+
for (const [key, matcher] of Object.entries(query)) {
|
|
71
86
|
const value = object[key as keyof T]
|
|
87
|
+
|
|
72
88
|
// if you add matching logic here, make sure you also update executeQuery,
|
|
73
89
|
// where initial data is pulled out of the indexes, since that requires different
|
|
74
90
|
// matching logic
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
if (isQueryValueMatcher(matcher)) {
|
|
92
|
+
if ('eq' in matcher && value !== matcher.eq) return false
|
|
93
|
+
if ('neq' in matcher && value === matcher.neq) return false
|
|
94
|
+
if ('gt' in matcher && (typeof value !== 'number' || value <= matcher.gt)) return false
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// It's a nested query
|
|
99
|
+
if (typeof value !== 'object' || value === null) return false
|
|
100
|
+
if (!objectMatchesQuery(matcher as QueryExpression<any>, value as any)) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
78
103
|
}
|
|
79
104
|
return true
|
|
80
105
|
}
|
|
@@ -100,6 +125,11 @@ export function objectMatchesQuery<T extends object>(query: QueryExpression<T>,
|
|
|
100
125
|
* const otherBookIds = executeQuery(store, 'book', {
|
|
101
126
|
* authorId: { neq: 'author:tolkien' }
|
|
102
127
|
* })
|
|
128
|
+
*
|
|
129
|
+
* // Query with nested properties
|
|
130
|
+
* const nestedQueryIds = executeQuery(store, 'book', {
|
|
131
|
+
* metadata: { sessionId: { eq: 'session:alpha' } }
|
|
132
|
+
* })
|
|
103
133
|
* ```
|
|
104
134
|
*
|
|
105
135
|
* @public
|
|
@@ -109,37 +139,49 @@ export function executeQuery<R extends UnknownRecord, TypeName extends R['typeNa
|
|
|
109
139
|
typeName: TypeName,
|
|
110
140
|
query: QueryExpression<Extract<R, { typeName: TypeName }>>
|
|
111
141
|
): Set<IdOf<Extract<R, { typeName: TypeName }>>> {
|
|
112
|
-
|
|
142
|
+
type S = Extract<R, { typeName: TypeName }>
|
|
143
|
+
|
|
144
|
+
// Extract all paths with matchers (flattens nested queries)
|
|
145
|
+
const matcherPaths = extractMatcherPaths(query)
|
|
146
|
+
|
|
147
|
+
// Build a set of matching IDs for each path
|
|
148
|
+
const matchIds = Object.fromEntries(matcherPaths.map(({ path }) => [path, new Set<IdOf<S>>()]))
|
|
149
|
+
|
|
150
|
+
// For each path, use the index to find matching IDs
|
|
151
|
+
for (const { path, matcher } of matcherPaths) {
|
|
152
|
+
const index = store.index(typeName, path as any)
|
|
113
153
|
|
|
114
|
-
for (const [k, matcher] of Object.entries(query)) {
|
|
115
154
|
if ('eq' in matcher) {
|
|
116
|
-
const index = store.index(typeName, k as any)
|
|
117
155
|
const ids = index.get().get(matcher.eq)
|
|
118
156
|
if (ids) {
|
|
119
157
|
for (const id of ids) {
|
|
120
|
-
matchIds[
|
|
158
|
+
matchIds[path].add(id)
|
|
121
159
|
}
|
|
122
160
|
}
|
|
123
161
|
} else if ('neq' in matcher) {
|
|
124
|
-
const index = store.index(typeName, k as any)
|
|
125
162
|
for (const [value, ids] of index.get()) {
|
|
126
163
|
if (value !== matcher.neq) {
|
|
127
164
|
for (const id of ids) {
|
|
128
|
-
matchIds[
|
|
165
|
+
matchIds[path].add(id)
|
|
129
166
|
}
|
|
130
167
|
}
|
|
131
168
|
}
|
|
132
169
|
} else if ('gt' in matcher) {
|
|
133
|
-
const index = store.index(typeName, k as any)
|
|
134
170
|
for (const [value, ids] of index.get()) {
|
|
135
|
-
if (value > matcher.gt) {
|
|
171
|
+
if (typeof value === 'number' && value > matcher.gt) {
|
|
136
172
|
for (const id of ids) {
|
|
137
|
-
matchIds[
|
|
173
|
+
matchIds[path].add(id)
|
|
138
174
|
}
|
|
139
175
|
}
|
|
140
176
|
}
|
|
141
177
|
}
|
|
178
|
+
|
|
179
|
+
// Short-circuit if this set is empty - intersection will be empty
|
|
180
|
+
if (matchIds[path].size === 0) {
|
|
181
|
+
return new Set()
|
|
182
|
+
}
|
|
142
183
|
}
|
|
143
184
|
|
|
144
|
-
|
|
185
|
+
// Intersect all the match sets
|
|
186
|
+
return intersectSets(Object.values(matchIds)) as Set<IdOf<S>>
|
|
145
187
|
}
|