@tldraw/store 4.2.0-next.47462e908ff5 → 4.2.0-next.54bc357bbff2
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/RecordType.js.map +2 -2
- 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/RecordType.mjs.map +2 -2
- 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/RecordType.ts +1 -1
- package/src/lib/StoreQueries.ts +102 -51
- package/src/lib/executeQuery.test.ts +928 -4
- package/src/lib/executeQuery.ts +78 -36
|
@@ -1,44 +1,68 @@
|
|
|
1
1
|
import { intersectSets } from "./setUtils.mjs";
|
|
2
|
+
function isQueryValueMatcher(value) {
|
|
3
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4
|
+
return "eq" in value || "neq" in value || "gt" in value;
|
|
5
|
+
}
|
|
6
|
+
function extractMatcherPaths(query, prefix = "") {
|
|
7
|
+
const paths = [];
|
|
8
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9
|
+
const currentPath = prefix ? `${prefix}\\${key}` : key;
|
|
10
|
+
if (isQueryValueMatcher(value)) {
|
|
11
|
+
paths.push({ path: currentPath, matcher: value });
|
|
12
|
+
} else if (typeof value === "object" && value !== null) {
|
|
13
|
+
paths.push(...extractMatcherPaths(value, currentPath));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return paths;
|
|
17
|
+
}
|
|
2
18
|
function objectMatchesQuery(query, object) {
|
|
3
|
-
for (const [key,
|
|
4
|
-
const matcher = _matcher;
|
|
19
|
+
for (const [key, matcher] of Object.entries(query)) {
|
|
5
20
|
const value = object[key];
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
if (isQueryValueMatcher(matcher)) {
|
|
22
|
+
if ("eq" in matcher && value !== matcher.eq) return false;
|
|
23
|
+
if ("neq" in matcher && value === matcher.neq) return false;
|
|
24
|
+
if ("gt" in matcher && (typeof value !== "number" || value <= matcher.gt)) return false;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value !== "object" || value === null) return false;
|
|
28
|
+
if (!objectMatchesQuery(matcher, value)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
9
31
|
}
|
|
10
32
|
return true;
|
|
11
33
|
}
|
|
12
34
|
function executeQuery(store, typeName, query) {
|
|
13
|
-
const
|
|
14
|
-
|
|
35
|
+
const matcherPaths = extractMatcherPaths(query);
|
|
36
|
+
const matchIds = Object.fromEntries(matcherPaths.map(({ path }) => [path, /* @__PURE__ */ new Set()]));
|
|
37
|
+
for (const { path, matcher } of matcherPaths) {
|
|
38
|
+
const index = store.index(typeName, path);
|
|
15
39
|
if ("eq" in matcher) {
|
|
16
|
-
const index = store.index(typeName, k);
|
|
17
40
|
const ids = index.get().get(matcher.eq);
|
|
18
41
|
if (ids) {
|
|
19
42
|
for (const id of ids) {
|
|
20
|
-
matchIds[
|
|
43
|
+
matchIds[path].add(id);
|
|
21
44
|
}
|
|
22
45
|
}
|
|
23
46
|
} else if ("neq" in matcher) {
|
|
24
|
-
const index = store.index(typeName, k);
|
|
25
47
|
for (const [value, ids] of index.get()) {
|
|
26
48
|
if (value !== matcher.neq) {
|
|
27
49
|
for (const id of ids) {
|
|
28
|
-
matchIds[
|
|
50
|
+
matchIds[path].add(id);
|
|
29
51
|
}
|
|
30
52
|
}
|
|
31
53
|
}
|
|
32
54
|
} else if ("gt" in matcher) {
|
|
33
|
-
const index = store.index(typeName, k);
|
|
34
55
|
for (const [value, ids] of index.get()) {
|
|
35
|
-
if (value > matcher.gt) {
|
|
56
|
+
if (typeof value === "number" && value > matcher.gt) {
|
|
36
57
|
for (const id of ids) {
|
|
37
|
-
matchIds[
|
|
58
|
+
matchIds[path].add(id);
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
61
|
}
|
|
41
62
|
}
|
|
63
|
+
if (matchIds[path].size === 0) {
|
|
64
|
+
return /* @__PURE__ */ new Set();
|
|
65
|
+
}
|
|
42
66
|
}
|
|
43
67
|
return intersectSets(Object.values(matchIds));
|
|
44
68
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/executeQuery.ts"],
|
|
4
|
-
"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 *\n * @public\n */\nexport type QueryExpression<R extends object> = {\n\t[k in keyof R & string]?: QueryValueMatcher<R[k]>\n\t
|
|
5
|
-
"mappings": "AACA,SAAS,qBAAqB;
|
|
4
|
+
"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"],
|
|
5
|
+
"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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/store",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (store).",
|
|
4
|
-
"version": "4.2.0-next.
|
|
4
|
+
"version": "4.2.0-next.54bc357bbff2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"context": "yarn run -T tsx ../../internal/scripts/context.ts"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@tldraw/state": "4.2.0-next.
|
|
48
|
-
"@tldraw/utils": "4.2.0-next.
|
|
47
|
+
"@tldraw/state": "4.2.0-next.54bc357bbff2",
|
|
48
|
+
"@tldraw/utils": "4.2.0-next.54bc357bbff2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": "^18.2.0 || ^19.0.0"
|
package/src/lib/RecordType.ts
CHANGED
|
@@ -105,7 +105,7 @@ export class RecordType<
|
|
|
105
105
|
const ephemeralKeySet = new Set<string>()
|
|
106
106
|
if (config.ephemeralKeys) {
|
|
107
107
|
for (const [key, isEphemeral] of objectMapEntries(config.ephemeralKeys)) {
|
|
108
|
-
if (isEphemeral) ephemeralKeySet.add(key)
|
|
108
|
+
if (isEphemeral) ephemeralKeySet.add(key as string)
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
this.ephemeralKeySet = ephemeralKeySet
|
package/src/lib/StoreQueries.ts
CHANGED
|
@@ -31,10 +31,7 @@ import { CollectionDiff } from './Store'
|
|
|
31
31
|
*
|
|
32
32
|
* @public
|
|
33
33
|
*/
|
|
34
|
-
export type RSIndexDiff<
|
|
35
|
-
R extends UnknownRecord,
|
|
36
|
-
Property extends string & keyof R = string & keyof R,
|
|
37
|
-
> = Map<R[Property], CollectionDiff<IdOf<R>>>
|
|
34
|
+
export type RSIndexDiff<R extends UnknownRecord> = Map<any, CollectionDiff<IdOf<R>>>
|
|
38
35
|
|
|
39
36
|
/**
|
|
40
37
|
* A type representing a reactive store index as a map from property values to sets of record IDs.
|
|
@@ -51,10 +48,7 @@ export type RSIndexDiff<
|
|
|
51
48
|
*
|
|
52
49
|
* @public
|
|
53
50
|
*/
|
|
54
|
-
export type RSIndexMap<
|
|
55
|
-
R extends UnknownRecord,
|
|
56
|
-
Property extends string & keyof R = string & keyof R,
|
|
57
|
-
> = Map<R[Property], Set<IdOf<R>>>
|
|
51
|
+
export type RSIndexMap<R extends UnknownRecord> = Map<any, Set<IdOf<R>>>
|
|
58
52
|
|
|
59
53
|
/**
|
|
60
54
|
* A reactive computed index that provides efficient lookups of records by property values.
|
|
@@ -71,10 +65,7 @@ export type RSIndexMap<
|
|
|
71
65
|
*
|
|
72
66
|
* @public
|
|
73
67
|
*/
|
|
74
|
-
export type RSIndex<
|
|
75
|
-
R extends UnknownRecord,
|
|
76
|
-
Property extends string & keyof R = string & keyof R,
|
|
77
|
-
> = Computed<RSIndexMap<R, Property>, RSIndexDiff<R, Property>>
|
|
68
|
+
export type RSIndex<R extends UnknownRecord> = Computed<RSIndexMap<R>, RSIndexDiff<R>>
|
|
78
69
|
|
|
79
70
|
/**
|
|
80
71
|
* A class that provides reactive querying capabilities for a record store.
|
|
@@ -121,6 +112,49 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
121
112
|
*/
|
|
122
113
|
private historyCache = new Map<string, Computed<number, RecordsDiff<R>>>()
|
|
123
114
|
|
|
115
|
+
/**
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
public getAllIdsForType<TypeName extends R['typeName']>(
|
|
119
|
+
typeName: TypeName
|
|
120
|
+
): Set<IdOf<Extract<R, { typeName: TypeName }>>> {
|
|
121
|
+
type S = Extract<R, { typeName: TypeName }>
|
|
122
|
+
const ids = new Set<IdOf<S>>()
|
|
123
|
+
for (const record of this.recordMap.values()) {
|
|
124
|
+
if (record.typeName === typeName) {
|
|
125
|
+
ids.add(record.id as IdOf<S>)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return ids
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
public getRecordById<TypeName extends R['typeName']>(
|
|
135
|
+
typeName: TypeName,
|
|
136
|
+
id: IdOf<Extract<R, { typeName: TypeName }>>
|
|
137
|
+
): Extract<R, { typeName: TypeName }> | undefined {
|
|
138
|
+
const record = this.recordMap.get(id as IdOf<R>)
|
|
139
|
+
if (record && record.typeName === typeName) {
|
|
140
|
+
return record as Extract<R, { typeName: TypeName }>
|
|
141
|
+
}
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Helper to extract nested property value using pre-split path parts.
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
private getNestedValue(obj: any, pathParts: string[]): any {
|
|
150
|
+
let current = obj
|
|
151
|
+
for (const part of pathParts) {
|
|
152
|
+
if (current == null || typeof current !== 'object') return undefined
|
|
153
|
+
current = current[part]
|
|
154
|
+
}
|
|
155
|
+
return current
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
/**
|
|
125
159
|
* Creates a reactive computed that tracks the change history for records of a specific type.
|
|
126
160
|
* The returned computed provides incremental diffs showing what records of the given type
|
|
@@ -237,8 +271,10 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
237
271
|
* The index automatically updates when records are added, updated, or removed, and results are cached
|
|
238
272
|
* for performance.
|
|
239
273
|
*
|
|
274
|
+
* Supports nested property paths using backslash separator (e.g., 'metadata\\sessionId').
|
|
275
|
+
*
|
|
240
276
|
* @param typeName - The type name of records to index
|
|
241
|
-
* @param
|
|
277
|
+
* @param path - The property name or backslash-delimited path to index by
|
|
242
278
|
* @returns A reactive computed containing the index map with change diffs
|
|
243
279
|
*
|
|
244
280
|
* @example
|
|
@@ -250,24 +286,24 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
250
286
|
* const authorBooks = booksByAuthor.get().get('author:leguin')
|
|
251
287
|
* console.log(authorBooks) // Set<RecordId<Book>>
|
|
252
288
|
*
|
|
253
|
-
* // Index by
|
|
254
|
-
* const
|
|
255
|
-
* const
|
|
289
|
+
* // Index by nested property using backslash separator
|
|
290
|
+
* const booksBySession = store.query.index('book', 'metadata\\sessionId')
|
|
291
|
+
* const sessionBooks = booksBySession.get().get('session:alpha')
|
|
256
292
|
* ```
|
|
257
293
|
*
|
|
258
294
|
* @public
|
|
259
295
|
*/
|
|
260
|
-
public index<
|
|
261
|
-
TypeName
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const cacheKey = typeName + ':' +
|
|
296
|
+
public index<TypeName extends R['typeName']>(
|
|
297
|
+
typeName: TypeName,
|
|
298
|
+
path: string
|
|
299
|
+
): RSIndex<Extract<R, { typeName: TypeName }>> {
|
|
300
|
+
const cacheKey = typeName + ':' + path
|
|
265
301
|
|
|
266
302
|
if (this.indexCache.has(cacheKey)) {
|
|
267
303
|
return this.indexCache.get(cacheKey) as any
|
|
268
304
|
}
|
|
269
305
|
|
|
270
|
-
const index = this.__uncached_createIndex(typeName,
|
|
306
|
+
const index = this.__uncached_createIndex(typeName, path)
|
|
271
307
|
|
|
272
308
|
this.indexCache.set(cacheKey, index as any)
|
|
273
309
|
|
|
@@ -278,40 +314,51 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
278
314
|
* Creates a new index without checking the cache. This method performs the actual work
|
|
279
315
|
* of building the reactive index computation that tracks property values to record ID sets.
|
|
280
316
|
*
|
|
317
|
+
* Supports nested property paths using backslash separator.
|
|
318
|
+
*
|
|
281
319
|
* @param typeName - The type name of records to index
|
|
282
|
-
* @param
|
|
320
|
+
* @param path - The property name or backslash-delimited path to index by
|
|
283
321
|
* @returns A reactive computed containing the index map with change diffs
|
|
284
322
|
*
|
|
285
323
|
* @internal
|
|
286
324
|
*/
|
|
287
|
-
__uncached_createIndex<
|
|
288
|
-
TypeName
|
|
289
|
-
|
|
290
|
-
|
|
325
|
+
__uncached_createIndex<TypeName extends R['typeName']>(
|
|
326
|
+
typeName: TypeName,
|
|
327
|
+
path: string
|
|
328
|
+
): RSIndex<Extract<R, { typeName: TypeName }>> {
|
|
291
329
|
type S = Extract<R, { typeName: TypeName }>
|
|
292
330
|
|
|
293
331
|
const typeHistory = this.filterHistory(typeName)
|
|
294
332
|
|
|
333
|
+
// Create closure for efficient property value extraction
|
|
334
|
+
const pathParts = path.split('\\')
|
|
335
|
+
const getPropertyValue =
|
|
336
|
+
pathParts.length > 1
|
|
337
|
+
? (obj: S) => this.getNestedValue(obj, pathParts)
|
|
338
|
+
: (obj: S) => obj[path as keyof S]
|
|
339
|
+
|
|
295
340
|
const fromScratch = () => {
|
|
296
341
|
// deref typeHistory early so that the first time the incremental version runs
|
|
297
342
|
// it gets a diff to work with instead of having to bail to this from-scratch version
|
|
298
343
|
typeHistory.get()
|
|
299
|
-
const res = new Map<
|
|
344
|
+
const res = new Map<any, Set<IdOf<S>>>()
|
|
300
345
|
for (const record of this.recordMap.values()) {
|
|
301
346
|
if (record.typeName === typeName) {
|
|
302
|
-
const value = (record as S)
|
|
303
|
-
if (
|
|
304
|
-
res.
|
|
347
|
+
const value = getPropertyValue(record as S)
|
|
348
|
+
if (value !== undefined) {
|
|
349
|
+
if (!res.has(value)) {
|
|
350
|
+
res.set(value, new Set())
|
|
351
|
+
}
|
|
352
|
+
res.get(value)!.add(record.id)
|
|
305
353
|
}
|
|
306
|
-
res.get(value)!.add(record.id)
|
|
307
354
|
}
|
|
308
355
|
}
|
|
309
356
|
|
|
310
357
|
return res
|
|
311
358
|
}
|
|
312
359
|
|
|
313
|
-
return computed<RSIndexMap<S
|
|
314
|
-
'index:' + typeName + ':' +
|
|
360
|
+
return computed<RSIndexMap<S>, RSIndexDiff<S>>(
|
|
361
|
+
'index:' + typeName + ':' + path,
|
|
315
362
|
(prevValue, lastComputedEpoch) => {
|
|
316
363
|
if (isUninitialized(prevValue)) return fromScratch()
|
|
317
364
|
|
|
@@ -322,7 +369,7 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
322
369
|
|
|
323
370
|
const setConstructors = new Map<any, IncrementalSetConstructor<IdOf<S>>>()
|
|
324
371
|
|
|
325
|
-
const add = (value:
|
|
372
|
+
const add = (value: any, id: IdOf<S>) => {
|
|
326
373
|
let setConstructor = setConstructors.get(value)
|
|
327
374
|
if (!setConstructor)
|
|
328
375
|
setConstructor = new IncrementalSetConstructor<IdOf<S>>(
|
|
@@ -332,7 +379,7 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
332
379
|
setConstructors.set(value, setConstructor)
|
|
333
380
|
}
|
|
334
381
|
|
|
335
|
-
const remove = (value:
|
|
382
|
+
const remove = (value: any, id: IdOf<S>) => {
|
|
336
383
|
let set = setConstructors.get(value)
|
|
337
384
|
if (!set) set = new IncrementalSetConstructor<IdOf<S>>(prevValue.get(value) ?? new Set())
|
|
338
385
|
set.remove(id)
|
|
@@ -342,30 +389,38 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
342
389
|
for (const changes of history) {
|
|
343
390
|
for (const record of objectMapValues(changes.added)) {
|
|
344
391
|
if (record.typeName === typeName) {
|
|
345
|
-
const value = (record as S)
|
|
346
|
-
|
|
392
|
+
const value = getPropertyValue(record as S)
|
|
393
|
+
if (value !== undefined) {
|
|
394
|
+
add(value, record.id)
|
|
395
|
+
}
|
|
347
396
|
}
|
|
348
397
|
}
|
|
349
398
|
for (const [from, to] of objectMapValues(changes.updated)) {
|
|
350
399
|
if (to.typeName === typeName) {
|
|
351
|
-
const prev = (from as S)
|
|
352
|
-
const next = (to as S)
|
|
400
|
+
const prev = getPropertyValue(from as S)
|
|
401
|
+
const next = getPropertyValue(to as S)
|
|
353
402
|
if (prev !== next) {
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
if (prev !== undefined) {
|
|
404
|
+
remove(prev, to.id)
|
|
405
|
+
}
|
|
406
|
+
if (next !== undefined) {
|
|
407
|
+
add(next, to.id)
|
|
408
|
+
}
|
|
356
409
|
}
|
|
357
410
|
}
|
|
358
411
|
}
|
|
359
412
|
for (const record of objectMapValues(changes.removed)) {
|
|
360
413
|
if (record.typeName === typeName) {
|
|
361
|
-
const value = (record as S)
|
|
362
|
-
|
|
414
|
+
const value = getPropertyValue(record as S)
|
|
415
|
+
if (value !== undefined) {
|
|
416
|
+
remove(value, record.id)
|
|
417
|
+
}
|
|
363
418
|
}
|
|
364
419
|
}
|
|
365
420
|
}
|
|
366
421
|
|
|
367
|
-
let nextValue: undefined | RSIndexMap<S
|
|
368
|
-
let nextDiff: undefined | RSIndexDiff<S
|
|
422
|
+
let nextValue: undefined | RSIndexMap<S> = undefined
|
|
423
|
+
let nextDiff: undefined | RSIndexDiff<S> = undefined
|
|
369
424
|
|
|
370
425
|
for (const [value, setConstructor] of setConstructors) {
|
|
371
426
|
const result = setConstructor.get()
|
|
@@ -513,11 +568,7 @@ export class StoreQueries<R extends UnknownRecord> {
|
|
|
513
568
|
typeHistory.get()
|
|
514
569
|
const query: QueryExpression<S> = queryCreator()
|
|
515
570
|
if (Object.keys(query).length === 0) {
|
|
516
|
-
|
|
517
|
-
for (const record of this.recordMap.values()) {
|
|
518
|
-
if (record.typeName === typeName) ids.add(record.id)
|
|
519
|
-
}
|
|
520
|
-
return ids
|
|
571
|
+
return this.getAllIdsForType(typeName)
|
|
521
572
|
}
|
|
522
573
|
|
|
523
574
|
return executeQuery(this, typeName, query)
|