@davidtkramer/convex-relations 0.2.0 → 0.4.1
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/README.md +33 -34
- package/dist/index.d.ts +20 -18
- package/dist/index.js +134 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,8 +126,6 @@ That works, but you are responsible for:
|
|
|
126
126
|
- [Error Semantics](#error-semantics)
|
|
127
127
|
- [Performance Characteristics](#performance-characteristics)
|
|
128
128
|
- [Comparison to `convex-helpers/server/relationships`](#comparison-to-convex-helpersserverrelationships)
|
|
129
|
-
- [Type Notes](#type-notes)
|
|
130
|
-
- [License](#license)
|
|
131
129
|
|
|
132
130
|
## Installation
|
|
133
131
|
|
|
@@ -157,12 +155,13 @@ wrappers. A minimal setup looks like this:
|
|
|
157
155
|
import { customCtx, customQuery } from "convex-helpers/server/customFunctions";
|
|
158
156
|
import { query as baseQuery } from "./_generated/server";
|
|
159
157
|
import type { DataModel } from "./_generated/dataModel";
|
|
158
|
+
import schema from "../schema";
|
|
160
159
|
import { createQueryFacade } from "@davidtkramer/convex-relations";
|
|
161
160
|
|
|
162
161
|
export const query = customQuery(
|
|
163
162
|
baseQuery,
|
|
164
163
|
customCtx((ctx: { db: any }) => ({
|
|
165
|
-
q: createQueryFacade<DataModel>(ctx.db),
|
|
164
|
+
q: createQueryFacade<DataModel>(ctx.db, schema),
|
|
166
165
|
})),
|
|
167
166
|
);
|
|
168
167
|
```
|
|
@@ -223,18 +222,26 @@ Single-field indexes accept a scalar. Compound indexes accept positional
|
|
|
223
222
|
arguments in index order. Zero-argument calls give you the indexed range so you
|
|
224
223
|
can filter, sort, paginate, or take a subset.
|
|
225
224
|
|
|
225
|
+
Because shorthand index methods like `.bySlug("...")` and
|
|
226
|
+
`.byPostIdAndStatus(...)` need the real indexed field names at runtime,
|
|
227
|
+
`createQueryFacade(...)` must be constructed with your Convex schema.
|
|
228
|
+
|
|
226
229
|
### `with(...)` builds nested result shapes
|
|
227
230
|
|
|
228
231
|
`with(...)` lets you attach additional fields to every document in a query. The
|
|
229
|
-
callback receives the current document
|
|
230
|
-
other query nodes or
|
|
232
|
+
callback receives the current document plus a small helper context, and returns
|
|
233
|
+
an object whose values can be plain sync values, other query nodes, or deferred
|
|
234
|
+
work via `defer(...)`.
|
|
231
235
|
|
|
232
236
|
```ts
|
|
233
237
|
const post = await ctx.q.posts
|
|
234
238
|
.bySlug("hello-world")
|
|
235
|
-
.with((post) => ({
|
|
239
|
+
.with((post, { defer }) => ({
|
|
236
240
|
author: ctx.q.authors.find(post.authorId),
|
|
237
241
|
comments: ctx.q.comments.byPostId(post._id).take(10),
|
|
242
|
+
readingTimeMinutes: defer(() =>
|
|
243
|
+
Math.ceil(post.body.split(/\s+/).length / 200),
|
|
244
|
+
),
|
|
238
245
|
}))
|
|
239
246
|
.unique();
|
|
240
247
|
```
|
|
@@ -259,26 +266,30 @@ of the result tree parallelizes across its sibling fields.
|
|
|
259
266
|
|
|
260
267
|
## API
|
|
261
268
|
|
|
262
|
-
### `createQueryFacade<DataModel>(db)`
|
|
269
|
+
### `createQueryFacade<DataModel>(db, schema)`
|
|
263
270
|
|
|
264
|
-
Creates a typed facade over your Convex `db`.
|
|
271
|
+
Creates a typed facade over your Convex `db`. Pass the runtime schema from
|
|
272
|
+
`defineSchema(...)` so indexed shorthand methods can map scalar and tuple
|
|
273
|
+
arguments onto the correct Convex index fields.
|
|
265
274
|
|
|
266
275
|
```ts
|
|
267
276
|
import { createQueryFacade } from "@davidtkramer/convex-relations";
|
|
268
277
|
import type { DataModel } from "./_generated/dataModel";
|
|
278
|
+
import schema from "../schema";
|
|
269
279
|
|
|
270
|
-
const q = createQueryFacade<DataModel>(ctx.db);
|
|
280
|
+
const q = createQueryFacade<DataModel>(ctx.db, schema);
|
|
271
281
|
```
|
|
272
282
|
|
|
273
|
-
### `
|
|
283
|
+
### `with(..., { defer })`
|
|
274
284
|
|
|
275
|
-
|
|
285
|
+
The `with(...)` callback context includes `defer`, which wraps arbitrary async
|
|
286
|
+
or sync work into the same lazy result tree as your relation queries.
|
|
276
287
|
|
|
277
288
|
```ts
|
|
278
289
|
const post = await q.posts
|
|
279
290
|
.bySlug("hello-world")
|
|
280
|
-
.with((post) => ({
|
|
281
|
-
readingTimeMinutes:
|
|
291
|
+
.with((post, { defer }) => ({
|
|
292
|
+
readingTimeMinutes: defer(() =>
|
|
282
293
|
Math.ceil(post.body.split(/\s+/).length / 200),
|
|
283
294
|
),
|
|
284
295
|
}))
|
|
@@ -364,10 +375,10 @@ Batch lookups skip missing rows.
|
|
|
364
375
|
```ts
|
|
365
376
|
const post = await q.posts
|
|
366
377
|
.bySlug("hello-world")
|
|
367
|
-
.with((post) => ({
|
|
378
|
+
.with((post, { defer }) => ({
|
|
368
379
|
author: q.authors.find(post.authorId),
|
|
369
380
|
comments: q.comments.byPostId(post._id).order("desc").take(10),
|
|
370
|
-
commentCount:
|
|
381
|
+
commentCount: defer(async () => {
|
|
371
382
|
const comments = await q.comments.byPostId(post._id).many();
|
|
372
383
|
return comments.length;
|
|
373
384
|
}),
|
|
@@ -407,12 +418,12 @@ const categories = await q.categories
|
|
|
407
418
|
This is essentially syntactic sugar for "run the source query, extract ids from
|
|
408
419
|
that field, then load the target rows for you."
|
|
409
420
|
|
|
410
|
-
You can also attach the source row
|
|
421
|
+
You can also attach the source row through the normal `with(...)` callback:
|
|
411
422
|
|
|
412
423
|
```ts
|
|
413
424
|
const categories = await q.categories
|
|
414
425
|
.through(q.postCategories.byPostId(postId).order("desc"), "categoryId")
|
|
415
|
-
.
|
|
426
|
+
.with((category, { source }) => ({ link: source }))
|
|
416
427
|
.many();
|
|
417
428
|
|
|
418
429
|
categories[0]?.link.postId;
|
|
@@ -427,7 +438,7 @@ timestamps.
|
|
|
427
438
|
```ts
|
|
428
439
|
const author = await q.authors
|
|
429
440
|
.through(q.posts.bySlug("hello-world"), "authorId")
|
|
430
|
-
.
|
|
441
|
+
.with((author, { source }) => ({ post: source }))
|
|
431
442
|
.unique();
|
|
432
443
|
|
|
433
444
|
author.post.slug;
|
|
@@ -445,12 +456,12 @@ const tags = await q.tags
|
|
|
445
456
|
.take(10),
|
|
446
457
|
"tagId",
|
|
447
458
|
)
|
|
448
|
-
.
|
|
459
|
+
.with((tag, { source }) => ({ link: source }))
|
|
449
460
|
.many();
|
|
450
461
|
```
|
|
451
462
|
|
|
452
463
|
After `through(...)`, you can keep shaping the target result with `with(...)`
|
|
453
|
-
and
|
|
464
|
+
and then choose a terminal like `many()` or `first()`.
|
|
454
465
|
|
|
455
466
|
## Terminals
|
|
456
467
|
|
|
@@ -517,10 +528,10 @@ const page = await q.posts.byAuthorId(authorId).paginate({
|
|
|
517
528
|
Within a single `with(...)` stage, every field in the returned object runs in parallel.
|
|
518
529
|
|
|
519
530
|
```ts
|
|
520
|
-
const post = await q.posts.find(postId).with((post) => ({
|
|
531
|
+
const post = await q.posts.find(postId).with((post, { defer }) => ({
|
|
521
532
|
author: q.authors.find(post.authorId),
|
|
522
533
|
comments: q.comments.byPostId(post._id).take(10),
|
|
523
|
-
categoryCount:
|
|
534
|
+
categoryCount: defer(async () => {
|
|
524
535
|
const categories = await q.categories
|
|
525
536
|
.through(q.postCategories.byPostId(post._id), "categoryId")
|
|
526
537
|
.many();
|
|
@@ -586,15 +597,3 @@ const categories = await getManyVia(
|
|
|
586
597
|
```
|
|
587
598
|
|
|
588
599
|
but also composes naturally with `with(...)`, `take(...)`, `firstOrNull()`, and typed nested traversal.
|
|
589
|
-
|
|
590
|
-
## Type Notes
|
|
591
|
-
|
|
592
|
-
- The facade is generic over your generated `DataModel`
|
|
593
|
-
- Table names, `_id` types, index names, and compound index prefixes are inferred
|
|
594
|
-
- Invalid table names and invalid index names are rejected at compile time
|
|
595
|
-
- Scalar shorthand is only allowed for single-field indexes
|
|
596
|
-
- Compound indexes use leading positional arguments
|
|
597
|
-
|
|
598
|
-
## License
|
|
599
|
-
|
|
600
|
-
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericDataModel, TableNamesInDataModel, DocumentByName, GenericTableInfo, FilterBuilder, ExpressionOrValue, NamedTableInfo, IndexNames, IndexRangeBuilder, NamedIndex, IndexRange, GenericDatabaseReader } from 'convex/server';
|
|
1
|
+
import { GenericDataModel, TableNamesInDataModel, DocumentByName, GenericTableInfo, FilterBuilder, ExpressionOrValue, NamedTableInfo, IndexNames, IndexRangeBuilder, NamedIndex, IndexRange, GenericDatabaseReader, SchemaDefinition } from 'convex/server';
|
|
2
2
|
import { GenericId } from 'convex/values';
|
|
3
3
|
|
|
4
4
|
type Simplify<T> = {
|
|
@@ -43,16 +43,20 @@ type QueryPlanHandle<DataModel extends GenericDataModel, Table extends AppTable<
|
|
|
43
43
|
readonly _plan: QueryPlan;
|
|
44
44
|
readonly _table: Table;
|
|
45
45
|
};
|
|
46
|
-
type WithSpec = Record<string,
|
|
47
|
-
type
|
|
48
|
-
type
|
|
46
|
+
type WithSpec = Record<string, unknown>;
|
|
47
|
+
type DeferredBuilder = <Output>(load: () => Promise<Output> | Output) => QueryNode<Output>;
|
|
48
|
+
type BaseWithContext = {
|
|
49
|
+
defer: DeferredBuilder;
|
|
50
|
+
};
|
|
51
|
+
type WithContext<SourceItem = never> = BaseWithContext & ([SourceItem] extends [never] ? {} : {
|
|
52
|
+
source: SourceItem;
|
|
53
|
+
});
|
|
54
|
+
type WithBuilder<ParentItem, Context = BaseWithContext, Spec extends WithSpec | undefined = WithSpec | undefined> = (parent: ParentItem, context: Context) => Spec;
|
|
55
|
+
type AnyWithBuilder<ParentItem, Context = BaseWithContext> = WithBuilder<ParentItem, Context, WithSpec | undefined>;
|
|
49
56
|
type BuiltWithSpec<Builder> = Builder extends (...args: any[]) => infer Spec ? Spec : never;
|
|
50
57
|
type ExpandWith<ParentItem, Builder> = Simplify<ParentItem & (BuiltWithSpec<Builder> extends Record<string, unknown> ? {
|
|
51
|
-
[K in keyof BuiltWithSpec<Builder>]: BuiltWithSpec<Builder>[K] extends QueryNode<infer Output> ? Output :
|
|
58
|
+
[K in keyof BuiltWithSpec<Builder>]: BuiltWithSpec<Builder>[K] extends QueryNode<infer Output> ? Output : BuiltWithSpec<Builder>[K];
|
|
52
59
|
} : {})>;
|
|
53
|
-
type AttachSource<ParentItem, SourceItem, SourceKey extends string> = Simplify<ParentItem & {
|
|
54
|
-
[K in SourceKey]: SourceItem;
|
|
55
|
-
}>;
|
|
56
60
|
type PaginationOptions = {
|
|
57
61
|
numItems: number;
|
|
58
62
|
cursor: string | null;
|
|
@@ -89,12 +93,10 @@ type ThroughSourceField<DataModel extends GenericDataModel, TargetTable extends
|
|
|
89
93
|
[Field in Extract<keyof SourceItem, string>]: IdTargetTable<DataModel, SourceItem[Field]> extends TargetTable ? Field : never;
|
|
90
94
|
}[Extract<keyof SourceItem, string>];
|
|
91
95
|
type ManyThroughQueryBuilder<Item, SourceItem = unknown> = QueryNode<Item[]> & ThroughNodeHandle<SourceItem, 'many'> & {
|
|
92
|
-
with<Builder extends AnyWithBuilder<Item
|
|
93
|
-
withSource<const SourceKey extends string>(key: SourceKey): ManyThroughQueryBuilder<AttachSource<Item, SourceItem, SourceKey>, SourceItem>;
|
|
96
|
+
with<Builder extends AnyWithBuilder<Item, WithContext<SourceItem>>>(withBuilder: Builder): ManyThroughQueryBuilder<ExpandWith<Item, Builder>, SourceItem>;
|
|
94
97
|
};
|
|
95
98
|
type SingleThroughQueryBuilder<Item, SourceItem, Nullable extends boolean> = QueryNode<Nullable extends true ? Item | null : Item> & ThroughNodeHandle<SourceItem, Nullable extends true ? 'nullableSingle' : 'single'> & {
|
|
96
|
-
with<Builder extends AnyWithBuilder<Item
|
|
97
|
-
withSource<const SourceKey extends string>(key: SourceKey): SingleThroughQueryBuilder<AttachSource<Item, SourceItem, SourceKey>, SourceItem, Nullable>;
|
|
99
|
+
with<Builder extends AnyWithBuilder<Item, WithContext<SourceItem>>>(withBuilder: Builder): SingleThroughQueryBuilder<ExpandWith<Item, Builder>, SourceItem, Nullable>;
|
|
98
100
|
};
|
|
99
101
|
type TableQueryFacade<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, Item = AppDoc<DataModel, Table>> = {
|
|
100
102
|
with<Builder extends AnyWithBuilder<Item>>(withBuilder: Builder): TableQueryFacade<DataModel, Table, ExpandWith<Item, Builder>>;
|
|
@@ -125,8 +127,7 @@ type TableBatchQueryFacade<DataModel extends GenericDataModel, Table extends App
|
|
|
125
127
|
many(): BatchQueryBuilder<Item>;
|
|
126
128
|
};
|
|
127
129
|
type ThroughQueryFacade<DataModel extends GenericDataModel, TargetTable extends AppTable<DataModel>, SourceItem, Item = AppDoc<DataModel, TargetTable>> = {
|
|
128
|
-
with<Builder extends AnyWithBuilder<Item
|
|
129
|
-
withSource<const SourceKey extends string>(key: SourceKey): ThroughQueryFacade<DataModel, TargetTable, SourceItem, AttachSource<Item, SourceItem, SourceKey>>;
|
|
130
|
+
with<Builder extends AnyWithBuilder<Item, WithContext<SourceItem>>>(withBuilder: Builder): ThroughQueryFacade<DataModel, TargetTable, SourceItem, ExpandWith<Item, Builder>>;
|
|
130
131
|
unique(): UniqueQueryBuilder<Item>;
|
|
131
132
|
uniqueOrNull(): UniqueOrNullQueryBuilder<Item>;
|
|
132
133
|
first(): FirstQueryBuilder<Item>;
|
|
@@ -164,18 +165,19 @@ type QuerySourcePlan = {
|
|
|
164
165
|
table: string;
|
|
165
166
|
index?: string;
|
|
166
167
|
selector?: unknown;
|
|
168
|
+
indexFields?: readonly string[];
|
|
167
169
|
} | {
|
|
168
170
|
kind: 'batch';
|
|
169
171
|
table: string;
|
|
170
172
|
index: string;
|
|
171
173
|
values: unknown[];
|
|
174
|
+
indexFields?: readonly string[];
|
|
172
175
|
};
|
|
173
176
|
type QueryPlan = {
|
|
174
177
|
source: QuerySourcePlan;
|
|
175
178
|
modifiers: QueryModifier[];
|
|
176
|
-
expanders: AnyWithBuilder<any>[];
|
|
179
|
+
expanders: AnyWithBuilder<any, any>[];
|
|
177
180
|
};
|
|
178
|
-
declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>): QueryFacade<DataModel>;
|
|
179
|
-
declare function compute<Output = unknown>(load: () => Promise<Output> | Output): QueryNode<Output>;
|
|
181
|
+
declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>, schema: SchemaDefinition<any, boolean>): QueryFacade<DataModel>;
|
|
180
182
|
|
|
181
|
-
export { type AppDoc, type AppTable, type QueryFacade, type RootIndexValueArg, type StrictRootIndexValueArg, type UserIndex,
|
|
183
|
+
export { type AppDoc, type AppTable, type QueryFacade, type RootIndexValueArg, type StrictRootIndexValueArg, type UserIndex, createQueryFacade };
|
package/dist/index.js
CHANGED
|
@@ -8,31 +8,46 @@ function createQueryNode(executeRoot) {
|
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
function createDeferredNode(load) {
|
|
12
|
+
return createQueryNode(async () => await load());
|
|
13
|
+
}
|
|
14
|
+
function isQueryNode(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && "_executeRoot" in value && typeof value._executeRoot === "function";
|
|
16
|
+
}
|
|
17
|
+
async function expandDoc(parent, withBuilder, context) {
|
|
18
|
+
const spec = withBuilder(parent, context) ?? {};
|
|
13
19
|
const entries = await Promise.all(
|
|
14
|
-
Object.entries(spec).map(
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
Object.entries(spec).map(async ([key, value]) => [
|
|
21
|
+
key,
|
|
22
|
+
isQueryNode(value) ? await value._executeRoot() : value
|
|
23
|
+
])
|
|
17
24
|
);
|
|
18
25
|
return {
|
|
19
26
|
...parent,
|
|
20
27
|
...Object.fromEntries(entries)
|
|
21
28
|
};
|
|
22
29
|
}
|
|
23
|
-
async function applyExpanders(item, expanders) {
|
|
30
|
+
async function applyExpanders(item, expanders, context) {
|
|
24
31
|
let current = item;
|
|
25
32
|
for (const expander of expanders) {
|
|
26
|
-
current = await expandDoc(current, expander);
|
|
33
|
+
current = await expandDoc(current, expander, context);
|
|
27
34
|
}
|
|
28
35
|
return current;
|
|
29
36
|
}
|
|
30
|
-
async function applyExpandersToMany(items, expanders) {
|
|
31
|
-
return await Promise.all(
|
|
37
|
+
async function applyExpandersToMany(items, expanders, getContext) {
|
|
38
|
+
return await Promise.all(
|
|
39
|
+
items.map((item, index) => applyExpanders(item, expanders, getContext(item, index)))
|
|
40
|
+
);
|
|
32
41
|
}
|
|
33
42
|
function buildQuery(makeQuery, modifiers) {
|
|
34
43
|
return modifiers.reduce((query, modifier) => modifier(query), makeQuery());
|
|
35
44
|
}
|
|
45
|
+
function createWithContext(source) {
|
|
46
|
+
return {
|
|
47
|
+
defer: createDeferredNode,
|
|
48
|
+
...source === void 0 ? {} : { source }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
36
51
|
function createPlan(source) {
|
|
37
52
|
return {
|
|
38
53
|
source,
|
|
@@ -52,16 +67,15 @@ function withExpander(plan, expander) {
|
|
|
52
67
|
expanders: [...plan.expanders, expander]
|
|
53
68
|
};
|
|
54
69
|
}
|
|
55
|
-
function normalizeIndexValues(index, value) {
|
|
70
|
+
function normalizeIndexValues(index, value, indexFields) {
|
|
56
71
|
if (Array.isArray(value)) {
|
|
57
|
-
|
|
58
|
-
return Object.fromEntries(fieldNames.map((field, index2) => [field, value[index2]]));
|
|
72
|
+
return Object.fromEntries(indexFields.map((field, index2) => [field, value[index2]]));
|
|
59
73
|
}
|
|
60
74
|
if (isPlainObject(value)) {
|
|
61
75
|
return value;
|
|
62
76
|
}
|
|
63
77
|
return {
|
|
64
|
-
[
|
|
78
|
+
[index === "by_id" ? "_id" : indexFields[0]]: value
|
|
65
79
|
};
|
|
66
80
|
}
|
|
67
81
|
function isPlainObject(value) {
|
|
@@ -74,19 +88,16 @@ function applyIndexValues(query, values) {
|
|
|
74
88
|
}
|
|
75
89
|
return current;
|
|
76
90
|
}
|
|
77
|
-
function
|
|
78
|
-
return inferFieldNamesFromIndex(index)[0];
|
|
79
|
-
}
|
|
80
|
-
function inferFieldNamesFromIndex(index) {
|
|
91
|
+
function requireIndexFields(index, indexFields) {
|
|
81
92
|
if (index === "by_id") {
|
|
82
93
|
return ["_id"];
|
|
83
94
|
}
|
|
84
|
-
if (
|
|
85
|
-
|
|
95
|
+
if (indexFields === void 0) {
|
|
96
|
+
throw new Error(`Missing schema metadata for index ${index}`);
|
|
86
97
|
}
|
|
87
|
-
|
|
98
|
+
return indexFields;
|
|
88
99
|
}
|
|
89
|
-
function createIndexedQuery(db, table, index, selector) {
|
|
100
|
+
function createIndexedQuery(db, table, index, selector, indexFields = []) {
|
|
90
101
|
const baseQuery = db.query(table);
|
|
91
102
|
if (index === void 0) {
|
|
92
103
|
return baseQuery;
|
|
@@ -100,9 +111,10 @@ function createIndexedQuery(db, table, index, selector) {
|
|
|
100
111
|
if (index === "by_id") {
|
|
101
112
|
return baseQuery.withIndex(index, (q) => q.eq("_id", selector));
|
|
102
113
|
}
|
|
114
|
+
const fields = requireIndexFields(index, indexFields);
|
|
103
115
|
return baseQuery.withIndex(
|
|
104
116
|
index,
|
|
105
|
-
(q) => applyIndexValues(q, normalizeIndexValues(index, selector))
|
|
117
|
+
(q) => applyIndexValues(q, normalizeIndexValues(index, selector, fields))
|
|
106
118
|
);
|
|
107
119
|
}
|
|
108
120
|
function sourceDescription(plan) {
|
|
@@ -139,7 +151,13 @@ async function* iterateDecoratedSourceItems(db, plan) {
|
|
|
139
151
|
const runtime = createPlanRuntime(db, plan);
|
|
140
152
|
const source = plan.source;
|
|
141
153
|
const query = buildQuery(
|
|
142
|
-
() => createIndexedQuery(
|
|
154
|
+
() => createIndexedQuery(
|
|
155
|
+
db,
|
|
156
|
+
source.table,
|
|
157
|
+
source.index,
|
|
158
|
+
source.selector,
|
|
159
|
+
source.indexFields
|
|
160
|
+
),
|
|
143
161
|
plan.modifiers
|
|
144
162
|
);
|
|
145
163
|
for await (const rawItem of query) {
|
|
@@ -176,18 +194,28 @@ function createPlanRuntime(db, plan) {
|
|
|
176
194
|
return {
|
|
177
195
|
many: async () => (await Promise.all(
|
|
178
196
|
source.values.map(
|
|
179
|
-
async (value) => await
|
|
197
|
+
async (value) => await queryManyByIndex(
|
|
198
|
+
db,
|
|
199
|
+
source.table,
|
|
200
|
+
source.index,
|
|
201
|
+
value,
|
|
202
|
+
source.indexFields
|
|
203
|
+
)
|
|
180
204
|
)
|
|
181
|
-
)).
|
|
182
|
-
(doc) => doc !== null
|
|
183
|
-
),
|
|
205
|
+
)).flat(),
|
|
184
206
|
mapOne: async (rawItem) => rawItem,
|
|
185
207
|
mapMany: async (rawItems) => rawItems,
|
|
186
208
|
missingMessages: {}
|
|
187
209
|
};
|
|
188
210
|
case "query": {
|
|
189
211
|
const runQuery = () => buildQuery(
|
|
190
|
-
() => createIndexedQuery(
|
|
212
|
+
() => createIndexedQuery(
|
|
213
|
+
db,
|
|
214
|
+
source.table,
|
|
215
|
+
source.index,
|
|
216
|
+
source.selector,
|
|
217
|
+
source.indexFields
|
|
218
|
+
),
|
|
191
219
|
plan.modifiers
|
|
192
220
|
);
|
|
193
221
|
return {
|
|
@@ -209,14 +237,18 @@ function createPlanRuntime(db, plan) {
|
|
|
209
237
|
async function decorateItem(plan, rawItem, item) {
|
|
210
238
|
let output = item;
|
|
211
239
|
if (plan.expanders.length > 0) {
|
|
212
|
-
output = await applyExpanders(output, plan.expanders);
|
|
240
|
+
output = await applyExpanders(output, plan.expanders, createWithContext());
|
|
213
241
|
}
|
|
214
242
|
return output;
|
|
215
243
|
}
|
|
216
244
|
async function decorateItems(plan, rawItems, items) {
|
|
217
245
|
let output = items;
|
|
218
246
|
if (plan.expanders.length > 0) {
|
|
219
|
-
output = await applyExpandersToMany(
|
|
247
|
+
output = await applyExpandersToMany(
|
|
248
|
+
output,
|
|
249
|
+
plan.expanders,
|
|
250
|
+
() => createWithContext()
|
|
251
|
+
);
|
|
220
252
|
}
|
|
221
253
|
return output;
|
|
222
254
|
}
|
|
@@ -310,22 +342,25 @@ function createManyQueryBuilder(executeRoot) {
|
|
|
310
342
|
_throughSourceKind: "many"
|
|
311
343
|
};
|
|
312
344
|
}
|
|
313
|
-
async function decorateThroughItem(expanders,
|
|
345
|
+
async function decorateThroughItem(expanders, pair) {
|
|
314
346
|
let output = pair.target;
|
|
315
|
-
if (sourceKey) {
|
|
316
|
-
output = { ...output, [sourceKey]: pair.source };
|
|
317
|
-
}
|
|
318
347
|
if (expanders.length > 0) {
|
|
319
|
-
output = await applyExpanders(
|
|
348
|
+
output = await applyExpanders(
|
|
349
|
+
output,
|
|
350
|
+
expanders,
|
|
351
|
+
createWithContext(pair.source)
|
|
352
|
+
);
|
|
320
353
|
}
|
|
321
354
|
return output;
|
|
322
355
|
}
|
|
323
|
-
async function decorateThroughItems(expanders,
|
|
324
|
-
let output = pairs.map(
|
|
325
|
-
({ source, target }) => sourceKey ? { ...target, [sourceKey]: source } : target
|
|
326
|
-
);
|
|
356
|
+
async function decorateThroughItems(expanders, pairs) {
|
|
357
|
+
let output = pairs.map(({ target }) => target);
|
|
327
358
|
if (expanders.length > 0) {
|
|
328
|
-
output = await applyExpandersToMany(
|
|
359
|
+
output = await applyExpandersToMany(
|
|
360
|
+
output,
|
|
361
|
+
expanders,
|
|
362
|
+
(_item, index) => createWithContext(pairs[index].source)
|
|
363
|
+
);
|
|
329
364
|
}
|
|
330
365
|
return output;
|
|
331
366
|
}
|
|
@@ -336,7 +371,7 @@ async function executeThroughCollectionMany(db, plan) {
|
|
|
336
371
|
plan.targetField,
|
|
337
372
|
sourceItems
|
|
338
373
|
);
|
|
339
|
-
return await decorateThroughItems(plan.expanders,
|
|
374
|
+
return await decorateThroughItems(plan.expanders, pairs);
|
|
340
375
|
}
|
|
341
376
|
async function executeThroughCollectionFirst(db, plan) {
|
|
342
377
|
const pair = (await collectThroughPairsUntil(
|
|
@@ -348,7 +383,7 @@ async function executeThroughCollectionFirst(db, plan) {
|
|
|
348
383
|
if (!pair) {
|
|
349
384
|
throw new Error(`Could not find first ${plan.targetTable} through ${sourceDescription(plan.sourcePlan)}`);
|
|
350
385
|
}
|
|
351
|
-
return await decorateThroughItem(plan.expanders,
|
|
386
|
+
return await decorateThroughItem(plan.expanders, pair);
|
|
352
387
|
}
|
|
353
388
|
async function executeThroughCollectionFirstOrNull(db, plan) {
|
|
354
389
|
const pair = (await collectThroughPairsUntil(
|
|
@@ -357,7 +392,7 @@ async function executeThroughCollectionFirstOrNull(db, plan) {
|
|
|
357
392
|
iterateDecoratedSourceItems(db, plan.sourcePlan),
|
|
358
393
|
1
|
|
359
394
|
))[0];
|
|
360
|
-
return pair ? await decorateThroughItem(plan.expanders,
|
|
395
|
+
return pair ? await decorateThroughItem(plan.expanders, pair) : null;
|
|
361
396
|
}
|
|
362
397
|
async function executeThroughCollectionUnique(db, plan) {
|
|
363
398
|
const pairs = await collectThroughPairsUntil(
|
|
@@ -373,7 +408,7 @@ async function executeThroughCollectionUnique(db, plan) {
|
|
|
373
408
|
if (!pair) {
|
|
374
409
|
throw new Error(`Could not find ${plan.targetTable} through ${sourceDescription(plan.sourcePlan)}`);
|
|
375
410
|
}
|
|
376
|
-
return await decorateThroughItem(plan.expanders,
|
|
411
|
+
return await decorateThroughItem(plan.expanders, pair);
|
|
377
412
|
}
|
|
378
413
|
async function executeThroughCollectionUniqueOrNull(db, plan) {
|
|
379
414
|
const pairs = await collectThroughPairsUntil(
|
|
@@ -385,7 +420,7 @@ async function executeThroughCollectionUniqueOrNull(db, plan) {
|
|
|
385
420
|
if (pairs.length > 1) {
|
|
386
421
|
throw new Error("unique() returned more than one result");
|
|
387
422
|
}
|
|
388
|
-
return pairs[0] ? await decorateThroughItem(plan.expanders,
|
|
423
|
+
return pairs[0] ? await decorateThroughItem(plan.expanders, pairs[0]) : null;
|
|
389
424
|
}
|
|
390
425
|
async function executeThroughManyNode(db, plan) {
|
|
391
426
|
const sourceItems = await plan.sourceNode._executeRoot();
|
|
@@ -394,7 +429,7 @@ async function executeThroughManyNode(db, plan) {
|
|
|
394
429
|
plan.targetField,
|
|
395
430
|
sourceItems
|
|
396
431
|
);
|
|
397
|
-
return await decorateThroughItems(plan.expanders,
|
|
432
|
+
return await decorateThroughItems(plan.expanders, pairs);
|
|
398
433
|
}
|
|
399
434
|
async function executeThroughSingleNode(db, plan, nullable) {
|
|
400
435
|
const sourceItem = await plan.sourceNode._executeRoot();
|
|
@@ -415,7 +450,7 @@ async function executeThroughSingleNode(db, plan, nullable) {
|
|
|
415
450
|
}
|
|
416
451
|
throw new Error(`Could not find ${plan.targetTable} through source query`);
|
|
417
452
|
}
|
|
418
|
-
return await decorateThroughItem(plan.expanders,
|
|
453
|
+
return await decorateThroughItem(plan.expanders, pair);
|
|
419
454
|
}
|
|
420
455
|
function withThroughCollectionExpander(plan, expander) {
|
|
421
456
|
return {
|
|
@@ -423,32 +458,17 @@ function withThroughCollectionExpander(plan, expander) {
|
|
|
423
458
|
expanders: [...plan.expanders, expander]
|
|
424
459
|
};
|
|
425
460
|
}
|
|
426
|
-
function withThroughCollectionSourceKey(plan, sourceKey) {
|
|
427
|
-
return {
|
|
428
|
-
...plan,
|
|
429
|
-
sourceKey
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
461
|
function withThroughNodeExpander(plan, expander) {
|
|
433
462
|
return {
|
|
434
463
|
...plan,
|
|
435
464
|
expanders: [...plan.expanders, expander]
|
|
436
465
|
};
|
|
437
466
|
}
|
|
438
|
-
function withThroughNodeSourceKey(plan, sourceKey) {
|
|
439
|
-
return {
|
|
440
|
-
...plan,
|
|
441
|
-
sourceKey
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
467
|
function createThroughCollectionFacade(db, plan) {
|
|
445
468
|
return {
|
|
446
469
|
with(withBuilder) {
|
|
447
470
|
return createThroughCollectionFacade(db, withThroughCollectionExpander(plan, withBuilder));
|
|
448
471
|
},
|
|
449
|
-
withSource(key) {
|
|
450
|
-
return createThroughCollectionFacade(db, withThroughCollectionSourceKey(plan, key));
|
|
451
|
-
},
|
|
452
472
|
unique() {
|
|
453
473
|
return createSingleQueryBuilder(
|
|
454
474
|
async () => await executeThroughCollectionUnique(db, plan),
|
|
@@ -489,9 +509,6 @@ function createThroughManyQueryBuilder(db, plan) {
|
|
|
489
509
|
db,
|
|
490
510
|
withThroughNodeExpander(plan, withBuilder)
|
|
491
511
|
);
|
|
492
|
-
},
|
|
493
|
-
withSource(key) {
|
|
494
|
-
return createThroughManyQueryBuilder(db, withThroughNodeSourceKey(plan, key));
|
|
495
512
|
}
|
|
496
513
|
};
|
|
497
514
|
}
|
|
@@ -501,9 +518,6 @@ function createThroughSingleQueryBuilder(db, plan, nullable) {
|
|
|
501
518
|
_throughSourceKind: nullable ? "nullableSingle" : "single",
|
|
502
519
|
with(withBuilder) {
|
|
503
520
|
return createThroughSingleQueryBuilder(db, withThroughNodeExpander(plan, withBuilder), nullable);
|
|
504
|
-
},
|
|
505
|
-
withSource(key) {
|
|
506
|
-
return createThroughSingleQueryBuilder(db, withThroughNodeSourceKey(plan, key), nullable);
|
|
507
521
|
}
|
|
508
522
|
};
|
|
509
523
|
}
|
|
@@ -592,20 +606,22 @@ function createIdPlan(table, id) {
|
|
|
592
606
|
id
|
|
593
607
|
});
|
|
594
608
|
}
|
|
595
|
-
function createQueryPlan(table, index, selector) {
|
|
609
|
+
function createQueryPlan(table, index, selector, indexFields) {
|
|
596
610
|
return createPlan({
|
|
597
611
|
kind: "query",
|
|
598
612
|
table,
|
|
599
613
|
index,
|
|
600
|
-
selector
|
|
614
|
+
selector,
|
|
615
|
+
indexFields
|
|
601
616
|
});
|
|
602
617
|
}
|
|
603
|
-
function createBatchPlan(table, index, values) {
|
|
618
|
+
function createBatchPlan(table, index, values, indexFields) {
|
|
604
619
|
return createPlan({
|
|
605
620
|
kind: "batch",
|
|
606
621
|
table,
|
|
607
622
|
index,
|
|
608
|
-
values
|
|
623
|
+
values,
|
|
624
|
+
indexFields
|
|
609
625
|
});
|
|
610
626
|
}
|
|
611
627
|
function isCollectionSource(value) {
|
|
@@ -626,7 +642,7 @@ function normalizeIndexSelectorArgs(args) {
|
|
|
626
642
|
}
|
|
627
643
|
return args;
|
|
628
644
|
}
|
|
629
|
-
function createTableNamespace(db, table) {
|
|
645
|
+
function createTableNamespace(db, table, resolveIndexFields) {
|
|
630
646
|
const rootFacade = createCollectionFacade(db, createQueryPlan(table));
|
|
631
647
|
const target = {
|
|
632
648
|
...rootFacade,
|
|
@@ -684,18 +700,49 @@ function createTableNamespace(db, table) {
|
|
|
684
700
|
createQueryPlan(
|
|
685
701
|
table,
|
|
686
702
|
prop,
|
|
687
|
-
normalizeIndexSelectorArgs(args)
|
|
703
|
+
normalizeIndexSelectorArgs(args),
|
|
704
|
+
resolveIndexFields(table, prop)
|
|
688
705
|
)
|
|
689
706
|
));
|
|
690
707
|
indexMethod.in = (values) => createBatchFacade(
|
|
691
708
|
db,
|
|
692
|
-
createBatchPlan(
|
|
709
|
+
createBatchPlan(
|
|
710
|
+
table,
|
|
711
|
+
prop,
|
|
712
|
+
values,
|
|
713
|
+
resolveIndexFields(table, prop)
|
|
714
|
+
)
|
|
693
715
|
);
|
|
694
716
|
return indexMethod;
|
|
695
717
|
}
|
|
696
718
|
});
|
|
697
719
|
}
|
|
698
|
-
function
|
|
720
|
+
function createIndexFieldResolver(schema) {
|
|
721
|
+
const cache = /* @__PURE__ */ new Map();
|
|
722
|
+
return (table, index) => {
|
|
723
|
+
if (index === "by_id") {
|
|
724
|
+
return ["_id"];
|
|
725
|
+
}
|
|
726
|
+
const cacheKey = `${table}:${index}`;
|
|
727
|
+
const cached = cache.get(cacheKey);
|
|
728
|
+
if (cached !== void 0) {
|
|
729
|
+
return cached;
|
|
730
|
+
}
|
|
731
|
+
const tableDefinition = schema.tables[table];
|
|
732
|
+
const indexes = tableDefinition?.[" indexes"]();
|
|
733
|
+
const match = indexes?.find(
|
|
734
|
+
(candidate) => candidate.indexDescriptor === index
|
|
735
|
+
);
|
|
736
|
+
if (match !== void 0 && Array.isArray(match.fields)) {
|
|
737
|
+
const fields = [...match.fields];
|
|
738
|
+
cache.set(cacheKey, fields);
|
|
739
|
+
return fields;
|
|
740
|
+
}
|
|
741
|
+
throw new Error(`Missing schema metadata for index ${table}.${index}`);
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function createQueryFacade(db, schema) {
|
|
745
|
+
const resolveIndexFields = createIndexFieldResolver(schema);
|
|
699
746
|
return new Proxy(
|
|
700
747
|
{},
|
|
701
748
|
{
|
|
@@ -703,24 +750,26 @@ function createQueryFacade(db) {
|
|
|
703
750
|
if (typeof prop !== "string" || RESERVED_PROMISE_KEYS.has(prop)) {
|
|
704
751
|
return void 0;
|
|
705
752
|
}
|
|
706
|
-
return createTableNamespace(
|
|
753
|
+
return createTableNamespace(
|
|
754
|
+
db,
|
|
755
|
+
prop,
|
|
756
|
+
resolveIndexFields
|
|
757
|
+
);
|
|
707
758
|
}
|
|
708
759
|
}
|
|
709
760
|
);
|
|
710
761
|
}
|
|
711
|
-
function
|
|
712
|
-
return createQueryNode(async () => await load());
|
|
713
|
-
}
|
|
714
|
-
async function queryUniqueByIndex(db, table, index, value) {
|
|
762
|
+
async function queryManyByIndex(db, table, index, value, indexFields) {
|
|
715
763
|
if (index === "by_id") {
|
|
716
|
-
|
|
764
|
+
const doc = await db.query(table).withIndex("by_id", (q) => q.eq("_id", value)).unique();
|
|
765
|
+
return doc ? [doc] : [];
|
|
717
766
|
}
|
|
767
|
+
const fields = requireIndexFields(index, indexFields);
|
|
718
768
|
return await db.query(table).withIndex(
|
|
719
769
|
index,
|
|
720
|
-
(q) => applyIndexValues(q, normalizeIndexValues(index, value))
|
|
721
|
-
).
|
|
770
|
+
(q) => applyIndexValues(q, normalizeIndexValues(index, value, fields))
|
|
771
|
+
).collect();
|
|
722
772
|
}
|
|
723
773
|
export {
|
|
724
|
-
compute,
|
|
725
774
|
createQueryFacade
|
|
726
775
|
};
|