@davidtkramer/convex-relations 0.1.0 → 0.4.0
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 +162 -102
- package/dist/index.d.ts +101 -57
- package/dist/index.js +342 -157
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,49 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img width="120" height="120" alt="image" src="https://github.com/user-attachments/assets/05619751-ea3a-4dd9-9bd7-3c231bd11d83" />
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<h1>Convex Relations</h1>
|
|
5
|
+
</div>
|
|
4
6
|
|
|
5
|
-
`convex-relations` is a server-side query facade for Convex. It
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
batch loading with `.in(...)`, and arbitrary computed fields with `compute(...)`.
|
|
9
|
-
|
|
10
|
-
It is designed for Convex query and mutation code, not frontend query clients.
|
|
11
|
-
|
|
12
|
-
## Table of Contents
|
|
13
|
-
|
|
14
|
-
- [Installation](#installation)
|
|
15
|
-
- [Example](#example)
|
|
16
|
-
- [Equivalent Convex Code](#equivalent-convex-code)
|
|
17
|
-
- [Quick Start](#quick-start)
|
|
18
|
-
- [Core Concepts](#core-concepts)
|
|
19
|
-
- [API](#api)
|
|
20
|
-
- [Table Access Patterns](#table-access-patterns)
|
|
21
|
-
- [Relation Expansion with `with(...)`](#relation-expansion-with-with)
|
|
22
|
-
- [Join Table Traversal with `via(...)`](#join-table-traversal-with-via)
|
|
23
|
-
- [Terminals](#terminals)
|
|
24
|
-
- [Error Semantics](#error-semantics)
|
|
25
|
-
- [Performance Characteristics](#performance-characteristics)
|
|
26
|
-
- [Comparison to `convex-helpers/server/relationships`](#comparison-to-convex-helpersserverrelationships)
|
|
27
|
-
- [Type Notes](#type-notes)
|
|
28
|
-
- [License](#license)
|
|
29
|
-
|
|
30
|
-
## Installation
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install @davidtkramer/convex-relations
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pnpm add @davidtkramer/convex-relations
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
bun add @davidtkramer/convex-relations
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
yarn add @davidtkramer/convex-relations
|
|
46
|
-
```
|
|
7
|
+
`convex-relations` is a server-side query facade for Convex. It lets you write
|
|
8
|
+
data loading code as a typed result tree instead of manually coordinating
|
|
9
|
+
lookups, parallelization, and response shaping by hand.
|
|
47
10
|
|
|
48
11
|
## Example
|
|
49
12
|
|
|
@@ -67,8 +30,7 @@ export const getPost = query({
|
|
|
67
30
|
}))
|
|
68
31
|
.take(10),
|
|
69
32
|
categories: ctx.q.categories
|
|
70
|
-
.
|
|
71
|
-
.byPostId(post._id)
|
|
33
|
+
.through(ctx.q.postCategories.byPostId(post._id), "categoryId")
|
|
72
34
|
.many(),
|
|
73
35
|
}))
|
|
74
36
|
.unique();
|
|
@@ -87,18 +49,18 @@ export const getPost = query({
|
|
|
87
49
|
});
|
|
88
50
|
```
|
|
89
51
|
|
|
90
|
-
This example shows
|
|
52
|
+
This example shows the core model:
|
|
91
53
|
|
|
92
|
-
- table-scoped access through `q.posts`, `q.comments`, `q.categories`
|
|
93
|
-
-
|
|
54
|
+
- table-scoped access through `q.posts`, `q.comments`, and `q.categories`
|
|
55
|
+
- indexes as first-class query methods like `.bySlug(...)` and `.byPostId(...)`
|
|
94
56
|
- nested relation expansion with `.with(...)` inside `.with(...)`
|
|
95
|
-
-
|
|
96
|
-
- parallel nested loading
|
|
97
|
-
- a final strongly typed result from one expression
|
|
57
|
+
- reference traversal with `.through(...)`
|
|
58
|
+
- parallel nested loading within each `with(...)`
|
|
59
|
+
- a final strongly typed, API-ready result from one expression
|
|
98
60
|
|
|
99
61
|
## Equivalent Convex Code
|
|
100
62
|
|
|
101
|
-
Without `convex-relations`, you end up assembling the same result shape by hand:
|
|
63
|
+
Without `convex-relations`, you end up assembling the same result shape by hand (or by agent 😉):
|
|
102
64
|
|
|
103
65
|
```ts
|
|
104
66
|
const post = await ctx.db
|
|
@@ -150,7 +112,38 @@ That works, but you are responsible for:
|
|
|
150
112
|
- remembering to manually `Promise.all(...)` nested relationships
|
|
151
113
|
- traversing join tables by hand
|
|
152
114
|
- assembling the final tree shape yourself for API responses
|
|
153
|
-
|
|
115
|
+
|
|
116
|
+
## Table of Contents
|
|
117
|
+
|
|
118
|
+
- [Installation](#installation)
|
|
119
|
+
- [Quick Start](#quick-start)
|
|
120
|
+
- [Core Concepts](#core-concepts)
|
|
121
|
+
- [API](#api)
|
|
122
|
+
- [Table Access Patterns](#table-access-patterns)
|
|
123
|
+
- [Relation Expansion with `with(...)`](#relation-expansion-with-with)
|
|
124
|
+
- [Reference Traversal with `through(...)`](#reference-traversal-with-through)
|
|
125
|
+
- [Terminals](#terminals)
|
|
126
|
+
- [Error Semantics](#error-semantics)
|
|
127
|
+
- [Performance Characteristics](#performance-characteristics)
|
|
128
|
+
- [Comparison to `convex-helpers/server/relationships`](#comparison-to-convex-helpersserverrelationships)
|
|
129
|
+
|
|
130
|
+
## Installation
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm install @davidtkramer/convex-relations
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pnpm add @davidtkramer/convex-relations
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bun add @davidtkramer/convex-relations
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
yarn add @davidtkramer/convex-relations
|
|
146
|
+
```
|
|
154
147
|
|
|
155
148
|
## Quick Start
|
|
156
149
|
|
|
@@ -162,12 +155,13 @@ wrappers. A minimal setup looks like this:
|
|
|
162
155
|
import { customCtx, customQuery } from "convex-helpers/server/customFunctions";
|
|
163
156
|
import { query as baseQuery } from "./_generated/server";
|
|
164
157
|
import type { DataModel } from "./_generated/dataModel";
|
|
158
|
+
import schema from "../schema";
|
|
165
159
|
import { createQueryFacade } from "@davidtkramer/convex-relations";
|
|
166
160
|
|
|
167
161
|
export const query = customQuery(
|
|
168
162
|
baseQuery,
|
|
169
163
|
customCtx((ctx: { db: any }) => ({
|
|
170
|
-
q: createQueryFacade<DataModel>(ctx.db),
|
|
164
|
+
q: createQueryFacade<DataModel>(ctx.db, schema),
|
|
171
165
|
})),
|
|
172
166
|
);
|
|
173
167
|
```
|
|
@@ -220,36 +214,82 @@ comments: defineTable({
|
|
|
220
214
|
const author = await ctx.q.authors.bySlug("ada-lovelace").unique();
|
|
221
215
|
const comments = await ctx.q.comments.byPostId(postId).many();
|
|
222
216
|
const approvedComments = await ctx.q.comments
|
|
223
|
-
.byPostIdAndStatus(
|
|
217
|
+
.byPostIdAndStatus(postId, "approved")
|
|
224
218
|
.many();
|
|
225
219
|
```
|
|
226
220
|
|
|
227
|
-
Single-field indexes accept a scalar. Compound indexes accept
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
Single-field indexes accept a scalar. Compound indexes accept positional
|
|
222
|
+
arguments in index order. Zero-argument calls give you the indexed range so you
|
|
223
|
+
can filter, sort, paginate, or take a subset.
|
|
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
|
+
|
|
229
|
+
### `with(...)` builds nested result shapes
|
|
230
|
+
|
|
231
|
+
`with(...)` lets you attach additional fields to every document in a query. The
|
|
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(...)`.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const post = await ctx.q.posts
|
|
238
|
+
.bySlug("hello-world")
|
|
239
|
+
.with((post, { defer }) => ({
|
|
240
|
+
author: ctx.q.authors.find(post.authorId),
|
|
241
|
+
comments: ctx.q.comments.byPostId(post._id).take(10),
|
|
242
|
+
readingTimeMinutes: defer(() =>
|
|
243
|
+
Math.ceil(post.body.split(/\s+/).length / 200),
|
|
244
|
+
),
|
|
245
|
+
}))
|
|
246
|
+
.unique();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
That returns a single object shaped like:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
{
|
|
253
|
+
...post,
|
|
254
|
+
author,
|
|
255
|
+
comments,
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This is the core idea behind the library: compose the data you want as a tree,
|
|
260
|
+
and `convex-relations` resolves and assembles that tree for you.
|
|
261
|
+
|
|
262
|
+
Within a single `with(...)`, sibling fields are resolved in parallel. If you
|
|
263
|
+
attach both `author` and `comments`, those branches start loading at the same
|
|
264
|
+
time. Nested `with(...)` calls preserve that behavior recursively, so each level
|
|
265
|
+
of the result tree parallelizes across its sibling fields.
|
|
230
266
|
|
|
231
267
|
## API
|
|
232
268
|
|
|
233
|
-
### `createQueryFacade<DataModel>(db)`
|
|
269
|
+
### `createQueryFacade<DataModel>(db, schema)`
|
|
234
270
|
|
|
235
|
-
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.
|
|
236
274
|
|
|
237
275
|
```ts
|
|
238
276
|
import { createQueryFacade } from "@davidtkramer/convex-relations";
|
|
239
277
|
import type { DataModel } from "./_generated/dataModel";
|
|
278
|
+
import schema from "../schema";
|
|
240
279
|
|
|
241
|
-
const q = createQueryFacade<DataModel>(ctx.db);
|
|
280
|
+
const q = createQueryFacade<DataModel>(ctx.db, schema);
|
|
242
281
|
```
|
|
243
282
|
|
|
244
|
-
### `
|
|
283
|
+
### `with(..., { defer })`
|
|
245
284
|
|
|
246
|
-
|
|
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.
|
|
247
287
|
|
|
248
288
|
```ts
|
|
249
289
|
const post = await q.posts
|
|
250
290
|
.bySlug("hello-world")
|
|
251
|
-
.with((post) => ({
|
|
252
|
-
readingTimeMinutes:
|
|
291
|
+
.with((post, { defer }) => ({
|
|
292
|
+
readingTimeMinutes: defer(() =>
|
|
253
293
|
Math.ceil(post.body.split(/\s+/).length / 200),
|
|
254
294
|
),
|
|
255
295
|
}))
|
|
@@ -291,16 +331,16 @@ Single-field indexes accept a scalar:
|
|
|
291
331
|
const author = await q.authors.bySlug("ada-lovelace").unique();
|
|
292
332
|
```
|
|
293
333
|
|
|
294
|
-
Compound indexes accept
|
|
334
|
+
Compound indexes accept leading positional arguments:
|
|
295
335
|
|
|
296
336
|
```ts
|
|
297
337
|
const comments = await q.comments
|
|
298
|
-
.
|
|
338
|
+
.byPostIdAndStatus(postId)
|
|
299
339
|
.order("desc")
|
|
300
340
|
.take(20);
|
|
301
341
|
|
|
302
342
|
const exactOrPrefix = await q.comments
|
|
303
|
-
.
|
|
343
|
+
.byPostIdAndStatus(postId, "approved")
|
|
304
344
|
.many();
|
|
305
345
|
```
|
|
306
346
|
|
|
@@ -309,8 +349,8 @@ const exactOrPrefix = await q.comments
|
|
|
309
349
|
You can also pass Convex's index selector callback:
|
|
310
350
|
|
|
311
351
|
```ts
|
|
312
|
-
const
|
|
313
|
-
.
|
|
352
|
+
const approvedComments = await q.comments
|
|
353
|
+
.byPostIdAndStatus((q) => q.eq("postId", postId).eq("status", "approved"))
|
|
314
354
|
.many();
|
|
315
355
|
```
|
|
316
356
|
|
|
@@ -335,10 +375,10 @@ Batch lookups skip missing rows.
|
|
|
335
375
|
```ts
|
|
336
376
|
const post = await q.posts
|
|
337
377
|
.bySlug("hello-world")
|
|
338
|
-
.with((post) => ({
|
|
378
|
+
.with((post, { defer }) => ({
|
|
339
379
|
author: q.authors.find(post.authorId),
|
|
340
380
|
comments: q.comments.byPostId(post._id).order("desc").take(10),
|
|
341
|
-
commentCount:
|
|
381
|
+
commentCount: defer(async () => {
|
|
342
382
|
const comments = await q.comments.byPostId(post._id).many();
|
|
343
383
|
return comments.length;
|
|
344
384
|
}),
|
|
@@ -362,33 +402,66 @@ const post = await q.posts
|
|
|
362
402
|
|
|
363
403
|
Each `with(...)` stage sees fields added by earlier stages.
|
|
364
404
|
|
|
365
|
-
##
|
|
405
|
+
## Reference Traversal with `through(...)`
|
|
366
406
|
|
|
367
|
-
Use `
|
|
407
|
+
Use `through(sourceQuery, foreignKeyField)` when another query already produces
|
|
408
|
+
rows that point at the table you want.
|
|
368
409
|
|
|
369
410
|
Given `postCategories { postId, categoryId }`, you can fetch categories for a post:
|
|
370
411
|
|
|
371
412
|
```ts
|
|
372
413
|
const categories = await q.categories
|
|
373
|
-
.
|
|
374
|
-
.byPostId(postId)
|
|
414
|
+
.through(q.postCategories.byPostId(postId), "categoryId")
|
|
375
415
|
.many();
|
|
376
416
|
```
|
|
377
417
|
|
|
378
|
-
|
|
418
|
+
This is essentially syntactic sugar for "run the source query, extract ids from
|
|
419
|
+
that field, then load the target rows for you."
|
|
420
|
+
|
|
421
|
+
You can also attach the source row through the normal `with(...)` callback:
|
|
379
422
|
|
|
380
423
|
```ts
|
|
381
424
|
const categories = await q.categories
|
|
382
|
-
.
|
|
383
|
-
.
|
|
384
|
-
.withSource("link")
|
|
425
|
+
.through(q.postCategories.byPostId(postId).order("desc"), "categoryId")
|
|
426
|
+
.with((category, { source }) => ({ link: source }))
|
|
385
427
|
.many();
|
|
386
428
|
|
|
387
429
|
categories[0]?.link.postId;
|
|
388
430
|
categories[0]?.link.categoryId;
|
|
389
431
|
```
|
|
390
432
|
|
|
391
|
-
This is useful when the
|
|
433
|
+
This is useful when the source table stores metadata like ordering, role, or
|
|
434
|
+
timestamps.
|
|
435
|
+
|
|
436
|
+
`through(...)` is not limited to join tables. Any compatible source query works:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
const author = await q.authors
|
|
440
|
+
.through(q.posts.bySlug("hello-world"), "authorId")
|
|
441
|
+
.with((author, { source }) => ({ post: source }))
|
|
442
|
+
.unique();
|
|
443
|
+
|
|
444
|
+
author.post.slug;
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Source-query shaping lives inside the `through(...)` argument:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
const tags = await q.tags
|
|
451
|
+
.through(
|
|
452
|
+
q.postTags
|
|
453
|
+
.byPostId(postId)
|
|
454
|
+
.filter((query) => query.eq(query.field("kind"), "primary"))
|
|
455
|
+
.order("desc")
|
|
456
|
+
.take(10),
|
|
457
|
+
"tagId",
|
|
458
|
+
)
|
|
459
|
+
.with((tag, { source }) => ({ link: source }))
|
|
460
|
+
.many();
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
After `through(...)`, you can keep shaping the target result with `with(...)`
|
|
464
|
+
and then choose a terminal like `many()` or `first()`.
|
|
392
465
|
|
|
393
466
|
## Terminals
|
|
394
467
|
|
|
@@ -447,7 +520,6 @@ const page = await q.posts.byAuthorId(authorId).paginate({
|
|
|
447
520
|
- `unique()` also throws if there are multiple matches
|
|
448
521
|
- `first()` throws if there is no match
|
|
449
522
|
- `findOrNull()`, `uniqueOrNull()`, and `firstOrNull()` return `null` instead
|
|
450
|
-
- `via(...).unique()` normalizes its duplicate error to include the target table and join index
|
|
451
523
|
|
|
452
524
|
## Performance Characteristics
|
|
453
525
|
|
|
@@ -456,13 +528,12 @@ const page = await q.posts.byAuthorId(authorId).paginate({
|
|
|
456
528
|
Within a single `with(...)` stage, every field in the returned object runs in parallel.
|
|
457
529
|
|
|
458
530
|
```ts
|
|
459
|
-
const post = await q.posts.find(postId).with((post) => ({
|
|
531
|
+
const post = await q.posts.find(postId).with((post, { defer }) => ({
|
|
460
532
|
author: q.authors.find(post.authorId),
|
|
461
533
|
comments: q.comments.byPostId(post._id).take(10),
|
|
462
|
-
categoryCount:
|
|
534
|
+
categoryCount: defer(async () => {
|
|
463
535
|
const categories = await q.categories
|
|
464
|
-
.
|
|
465
|
-
.byPostId(post._id)
|
|
536
|
+
.through(q.postCategories.byPostId(post._id), "categoryId")
|
|
466
537
|
.many();
|
|
467
538
|
return categories.length;
|
|
468
539
|
}),
|
|
@@ -489,7 +560,9 @@ q.posts
|
|
|
489
560
|
|
|
490
561
|
The second stage waits for the first stage, because it depends on fields added earlier.
|
|
491
562
|
|
|
492
|
-
`
|
|
563
|
+
`through(...)` currently resolves target documents by fetching the source rows
|
|
564
|
+
first, then loading each target document individually. This is correct and
|
|
565
|
+
predictable, but it is not a single batched join at the database level.
|
|
493
566
|
|
|
494
567
|
### Practical guidance
|
|
495
568
|
|
|
@@ -507,8 +580,7 @@ This:
|
|
|
507
580
|
|
|
508
581
|
```ts
|
|
509
582
|
const categories = await q.categories
|
|
510
|
-
.
|
|
511
|
-
.byPostId(postId)
|
|
583
|
+
.through(q.postCategories.byPostId(postId), "categoryId")
|
|
512
584
|
.many();
|
|
513
585
|
```
|
|
514
586
|
|
|
@@ -525,15 +597,3 @@ const categories = await getManyVia(
|
|
|
525
597
|
```
|
|
526
598
|
|
|
527
599
|
but also composes naturally with `with(...)`, `take(...)`, `firstOrNull()`, and typed nested traversal.
|
|
528
|
-
|
|
529
|
-
## Type Notes
|
|
530
|
-
|
|
531
|
-
- The facade is generic over your generated `DataModel`
|
|
532
|
-
- Table names, `_id` types, index names, and compound index prefixes are inferred
|
|
533
|
-
- Invalid table names and invalid index names are rejected at compile time
|
|
534
|
-
- Scalar shorthand is only allowed for single-field indexes
|
|
535
|
-
- Compound indexes require a valid prefix object
|
|
536
|
-
|
|
537
|
-
## License
|
|
538
|
-
|
|
539
|
-
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericDataModel, TableNamesInDataModel, DocumentByName,
|
|
1
|
+
import { GenericDataModel, TableNamesInDataModel, DocumentByName, GenericTableInfo, FilterBuilder, ExpressionOrValue, NamedTableInfo, IndexNames, IndexRangeBuilder, NamedIndex, IndexRange, GenericDatabaseReader } from 'convex/server';
|
|
2
2
|
import { GenericId } from 'convex/values';
|
|
3
3
|
|
|
4
4
|
type Simplify<T> = {
|
|
@@ -8,38 +8,55 @@ type AppTable<DataModel extends GenericDataModel> = TableNamesInDataModel<DataMo
|
|
|
8
8
|
type AppDoc<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = DocumentByName<DataModel, Table>;
|
|
9
9
|
type UserIndex<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = Exclude<IndexNames<NamedTableInfo<DataModel, Table>>, 'by_creation_time'> & string;
|
|
10
10
|
type RawIndexFields<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = NamedIndex<NamedTableInfo<DataModel, Table>, IndexName>;
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
infer Field extends string,
|
|
11
|
+
type StripCreationTime<Fields extends readonly string[]> = Fields extends readonly [
|
|
12
|
+
...infer Rest extends readonly string[],
|
|
14
13
|
'_creationTime'
|
|
14
|
+
] ? Rest : Fields;
|
|
15
|
+
type UserIndexFields<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = StripCreationTime<RawIndexFields<DataModel, Table, IndexName>>;
|
|
16
|
+
type SingleIndexField<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = UserIndexFields<DataModel, Table, IndexName> extends readonly [
|
|
17
|
+
infer Field extends string
|
|
15
18
|
] ? Field : never;
|
|
16
|
-
type
|
|
19
|
+
type TuplePrefixArgs<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, Fields extends readonly string[], Seen extends readonly string[] = [], SeenValues extends readonly unknown[] = []> = Fields extends readonly [
|
|
17
20
|
infer Head extends string,
|
|
18
21
|
...infer Tail extends readonly string[]
|
|
19
|
-
] ?
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
] ? [...SeenValues, AppDoc<DataModel, Table>[Head]] | TuplePrefixArgs<DataModel, Table, Tail, [
|
|
23
|
+
...Seen,
|
|
24
|
+
Head
|
|
25
|
+
], [
|
|
26
|
+
...SeenValues,
|
|
27
|
+
AppDoc<DataModel, Table>[Head]
|
|
28
|
+
]> : never;
|
|
29
|
+
type PositionalIndexArgs<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = TuplePrefixArgs<DataModel, Table, UserIndexFields<DataModel, Table, IndexName>>;
|
|
30
|
+
type RootIndexValueArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = PositionalIndexArgs<DataModel, Table, IndexName> | (SingleIndexField<DataModel, Table, IndexName> extends never ? never : AppDoc<DataModel, Table>[SingleIndexField<DataModel, Table, IndexName>]);
|
|
31
|
+
type StrictRootIndexValueArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>, Value extends RootIndexValueArg<DataModel, Table, IndexName>> = Value;
|
|
25
32
|
type TableIndexName<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = UserIndex<DataModel, Table> | 'by_id';
|
|
26
|
-
type
|
|
27
|
-
type
|
|
33
|
+
type UserIndexArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = SingleIndexField<DataModel, Table, IndexName> extends never ? PositionalIndexArgs<DataModel, Table, IndexName> : AppDoc<DataModel, Table>[SingleIndexField<DataModel, Table, IndexName>];
|
|
34
|
+
type TableIndexValueArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends TableIndexName<DataModel, Table>> = IndexName extends 'by_id' ? GenericId<Table> : IndexName extends UserIndex<DataModel, Table> ? UserIndexArg<DataModel, Table, IndexName> : never;
|
|
35
|
+
type StrictTableIndexValueArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends TableIndexName<DataModel, Table>, Value extends TableIndexValueArg<DataModel, Table, IndexName>> = Value;
|
|
36
|
+
type TableIndexInvocationArgs<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends TableIndexName<DataModel, Table>> = IndexName extends 'by_id' ? [GenericId<Table>] : IndexName extends UserIndex<DataModel, Table> ? PositionalIndexArgs<DataModel, Table, IndexName> : never;
|
|
28
37
|
type IdTargetTable<DataModel extends GenericDataModel, Value> = Value extends GenericId<infer Table extends AppTable<DataModel>> ? Table : never;
|
|
29
|
-
type JoinTargetTable<DataModel extends GenericDataModel, JoinTable extends AppTable<DataModel>, TargetField extends Extract<keyof AppDoc<DataModel, JoinTable>, string>> = IdTargetTable<DataModel, AppDoc<DataModel, JoinTable>[TargetField]>;
|
|
30
38
|
type QueryNode<Output> = PromiseLike<Output> & {
|
|
31
39
|
readonly _executeRoot: () => Promise<Output>;
|
|
32
40
|
};
|
|
33
|
-
type
|
|
34
|
-
type
|
|
35
|
-
|
|
41
|
+
type TableInfo<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = NamedTableInfo<DataModel, Table>;
|
|
42
|
+
type QueryPlanHandle<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = {
|
|
43
|
+
readonly _plan: QueryPlan;
|
|
44
|
+
readonly _table: Table;
|
|
45
|
+
};
|
|
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>;
|
|
36
56
|
type BuiltWithSpec<Builder> = Builder extends (...args: any[]) => infer Spec ? Spec : never;
|
|
37
57
|
type ExpandWith<ParentItem, Builder> = Simplify<ParentItem & (BuiltWithSpec<Builder> extends Record<string, unknown> ? {
|
|
38
|
-
[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];
|
|
39
59
|
} : {})>;
|
|
40
|
-
type AttachSource<ParentItem, SourceItem, SourceKey extends string> = Simplify<ParentItem & {
|
|
41
|
-
[K in SourceKey]: SourceItem;
|
|
42
|
-
}>;
|
|
43
60
|
type PaginationOptions = {
|
|
44
61
|
numItems: number;
|
|
45
62
|
cursor: string | null;
|
|
@@ -49,81 +66,89 @@ type PaginatedResult<Item> = {
|
|
|
49
66
|
isDone: boolean;
|
|
50
67
|
continueCursor: string;
|
|
51
68
|
};
|
|
52
|
-
type QueryFilter = (q:
|
|
53
|
-
type
|
|
54
|
-
type
|
|
69
|
+
type QueryFilter<TableInfo extends GenericTableInfo> = (q: FilterBuilder<TableInfo>) => ExpressionOrValue<boolean>;
|
|
70
|
+
type QueryModifier = (query: any) => any;
|
|
71
|
+
type IndexSelector<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = (q: IndexRangeBuilder<AppDoc<DataModel, Table>, NamedIndex<TableInfo<DataModel, Table>, IndexName>>) => IndexRange;
|
|
72
|
+
type SingleNodeKind<Nullable extends boolean> = Nullable extends true ? 'nullableSingle' : 'single';
|
|
73
|
+
type SingleQueryBuilder<Item, Nullable extends boolean> = QueryNode<Nullable extends true ? Item | null : Item> & ThroughNodeHandle<Item, SingleNodeKind<Nullable>>;
|
|
74
|
+
type ExpandableSingleQueryBuilder<Item, Nullable extends boolean> = SingleQueryBuilder<Item, Nullable> & {
|
|
55
75
|
with<Builder extends AnyWithBuilder<Item>>(withBuilder: Builder): ExpandableSingleQueryBuilder<ExpandWith<Item, Builder>, Nullable>;
|
|
56
76
|
};
|
|
57
|
-
type SingleQueryBuilder<Item, Nullable extends boolean> = QueryNode<Nullable extends true ? Item | null : Item>;
|
|
58
77
|
type UniqueQueryBuilder<Item> = SingleQueryBuilder<Item, false>;
|
|
59
78
|
type UniqueOrNullQueryBuilder<Item> = SingleQueryBuilder<Item, true>;
|
|
60
79
|
type FirstQueryBuilder<Item> = SingleQueryBuilder<Item, false>;
|
|
61
80
|
type FirstOrNullQueryBuilder<Item> = SingleQueryBuilder<Item, true>;
|
|
62
81
|
type FindQueryBuilder<Item> = ExpandableSingleQueryBuilder<Item, false>;
|
|
63
82
|
type FindOrNullQueryBuilder<Item> = ExpandableSingleQueryBuilder<Item, true>;
|
|
64
|
-
type ManyQueryBuilder<Item> = QueryNode<Item[]>;
|
|
65
|
-
type BatchQueryBuilder<Item> =
|
|
66
|
-
type
|
|
83
|
+
type ManyQueryBuilder<Item> = QueryNode<Item[]> & ThroughNodeHandle<Item, 'many'>;
|
|
84
|
+
type BatchQueryBuilder<Item> = ManyQueryBuilder<Item>;
|
|
85
|
+
type ThroughSourceNodeKind = 'many' | 'single' | 'nullableSingle';
|
|
86
|
+
type ThroughNodeHandle<SourceItem, Kind extends ThroughSourceNodeKind> = {
|
|
87
|
+
readonly _throughSourceKind: Kind;
|
|
88
|
+
readonly _throughSourceType?: SourceItem;
|
|
89
|
+
};
|
|
90
|
+
type AnyManySourceNode<SourceItem> = QueryNode<SourceItem[]> & ThroughNodeHandle<SourceItem, 'many'>;
|
|
91
|
+
type AnySingleSourceNode<SourceItem, Nullable extends boolean> = QueryNode<Nullable extends true ? SourceItem | null : SourceItem> & ThroughNodeHandle<SourceItem, SingleNodeKind<Nullable>>;
|
|
92
|
+
type ThroughSourceField<DataModel extends GenericDataModel, TargetTable extends AppTable<DataModel>, SourceItem> = {
|
|
93
|
+
[Field in Extract<keyof SourceItem, string>]: IdTargetTable<DataModel, SourceItem[Field]> extends TargetTable ? Field : never;
|
|
94
|
+
}[Extract<keyof SourceItem, string>];
|
|
95
|
+
type ManyThroughQueryBuilder<Item, SourceItem = unknown> = QueryNode<Item[]> & ThroughNodeHandle<SourceItem, 'many'> & {
|
|
96
|
+
with<Builder extends AnyWithBuilder<Item, WithContext<SourceItem>>>(withBuilder: Builder): ManyThroughQueryBuilder<ExpandWith<Item, Builder>, SourceItem>;
|
|
97
|
+
};
|
|
98
|
+
type SingleThroughQueryBuilder<Item, SourceItem, Nullable extends boolean> = QueryNode<Nullable extends true ? Item | null : Item> & ThroughNodeHandle<SourceItem, Nullable extends true ? 'nullableSingle' : 'single'> & {
|
|
99
|
+
with<Builder extends AnyWithBuilder<Item, WithContext<SourceItem>>>(withBuilder: Builder): SingleThroughQueryBuilder<ExpandWith<Item, Builder>, SourceItem, Nullable>;
|
|
100
|
+
};
|
|
67
101
|
type TableQueryFacade<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, Item = AppDoc<DataModel, Table>> = {
|
|
68
102
|
with<Builder extends AnyWithBuilder<Item>>(withBuilder: Builder): TableQueryFacade<DataModel, Table, ExpandWith<Item, Builder>>;
|
|
69
103
|
order(direction: 'asc' | 'desc'): TableQueryFacade<DataModel, Table, Item>;
|
|
70
|
-
filter(filterer: QueryFilter): TableQueryFacade<DataModel, Table, Item>;
|
|
104
|
+
filter(filterer: QueryFilter<TableInfo<DataModel, Table>>): TableQueryFacade<DataModel, Table, Item>;
|
|
71
105
|
unique(): UniqueQueryBuilder<Item>;
|
|
72
106
|
uniqueOrNull(): UniqueOrNullQueryBuilder<Item>;
|
|
73
107
|
first(): FirstQueryBuilder<Item>;
|
|
74
108
|
firstOrNull(): FirstOrNullQueryBuilder<Item>;
|
|
75
|
-
take(count: number):
|
|
109
|
+
take(count: number): ManyQueryBuilder<Item>;
|
|
76
110
|
paginate(opts: PaginationOptions): Promise<PaginatedResult<Item>>;
|
|
77
111
|
many(): ManyQueryBuilder<Item>;
|
|
78
|
-
}
|
|
112
|
+
} & QueryPlanHandle<DataModel, Table>;
|
|
79
113
|
type TableRangeQueryFacade<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, Item = AppDoc<DataModel, Table>> = {
|
|
80
114
|
with<Builder extends AnyWithBuilder<Item>>(withBuilder: Builder): TableRangeQueryFacade<DataModel, Table, ExpandWith<Item, Builder>>;
|
|
81
115
|
order(direction: 'asc' | 'desc'): TableRangeQueryFacade<DataModel, Table, Item>;
|
|
82
|
-
filter(filterer: QueryFilter): TableRangeQueryFacade<DataModel, Table, Item>;
|
|
116
|
+
filter(filterer: QueryFilter<TableInfo<DataModel, Table>>): TableRangeQueryFacade<DataModel, Table, Item>;
|
|
83
117
|
unique(): UniqueQueryBuilder<Item>;
|
|
84
118
|
uniqueOrNull(): UniqueOrNullQueryBuilder<Item>;
|
|
85
119
|
first(): FirstQueryBuilder<Item>;
|
|
86
120
|
firstOrNull(): FirstOrNullQueryBuilder<Item>;
|
|
87
|
-
take(count: number):
|
|
121
|
+
take(count: number): ManyQueryBuilder<Item>;
|
|
88
122
|
paginate(opts: PaginationOptions): Promise<PaginatedResult<Item>>;
|
|
89
123
|
many(): ManyQueryBuilder<Item>;
|
|
90
|
-
}
|
|
124
|
+
} & QueryPlanHandle<DataModel, Table>;
|
|
91
125
|
type TableBatchQueryFacade<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, Item = AppDoc<DataModel, Table>> = {
|
|
92
126
|
with<Builder extends AnyWithBuilder<Item>>(withBuilder: Builder): TableBatchQueryFacade<DataModel, Table, ExpandWith<Item, Builder>>;
|
|
93
127
|
many(): BatchQueryBuilder<Item>;
|
|
94
128
|
};
|
|
95
|
-
type
|
|
96
|
-
with<Builder extends AnyWithBuilder<Item
|
|
97
|
-
order(direction: 'asc' | 'desc'): ViaQueryFacade<DataModel, TargetTable, JoinTable, Item>;
|
|
98
|
-
filter(filterer: QueryFilter): ViaQueryFacade<DataModel, TargetTable, JoinTable, Item>;
|
|
99
|
-
withSource<const SourceKey extends string>(key: SourceKey): ViaQueryFacade<DataModel, TargetTable, JoinTable, AttachSource<Item, AppDoc<DataModel, JoinTable>, SourceKey>>;
|
|
129
|
+
type ThroughQueryFacade<DataModel extends GenericDataModel, TargetTable extends AppTable<DataModel>, SourceItem, Item = AppDoc<DataModel, TargetTable>> = {
|
|
130
|
+
with<Builder extends AnyWithBuilder<Item, WithContext<SourceItem>>>(withBuilder: Builder): ThroughQueryFacade<DataModel, TargetTable, SourceItem, ExpandWith<Item, Builder>>;
|
|
100
131
|
unique(): UniqueQueryBuilder<Item>;
|
|
101
132
|
uniqueOrNull(): UniqueOrNullQueryBuilder<Item>;
|
|
102
133
|
first(): FirstQueryBuilder<Item>;
|
|
103
134
|
firstOrNull(): FirstOrNullQueryBuilder<Item>;
|
|
104
|
-
|
|
105
|
-
paginate(opts: PaginationOptions): Promise<PaginatedResult<Item>>;
|
|
106
|
-
many(): ManyViaQueryBuilder<Item>;
|
|
107
|
-
};
|
|
108
|
-
type ValidViaTargetField<DataModel extends GenericDataModel, JoinTable extends AppTable<DataModel>, TargetTable extends AppTable<DataModel>> = {
|
|
109
|
-
[Field in Extract<keyof AppDoc<DataModel, JoinTable>, string>]: JoinTargetTable<DataModel, JoinTable, Field> extends TargetTable ? Field : never;
|
|
110
|
-
}[Extract<keyof AppDoc<DataModel, JoinTable>, string>];
|
|
111
|
-
type ViaIndexNamespace<DataModel extends GenericDataModel, TargetTable extends AppTable<DataModel>, JoinTable extends AppTable<DataModel>> = {
|
|
112
|
-
[IndexName in UserIndex<DataModel, JoinTable>]: {
|
|
113
|
-
(): ViaQueryFacade<DataModel, TargetTable, JoinTable>;
|
|
114
|
-
<const Value extends RootIndexValueArg<DataModel, JoinTable, IndexName>>(value: StrictRootIndexValueArg<DataModel, JoinTable, IndexName, Value>): ViaQueryFacade<DataModel, TargetTable, JoinTable>;
|
|
115
|
-
(selector: IndexSelector): ViaQueryFacade<DataModel, TargetTable, JoinTable>;
|
|
116
|
-
};
|
|
135
|
+
many(): ManyQueryBuilder<Item>;
|
|
117
136
|
};
|
|
137
|
+
type ThroughCollectionSource<DataModel extends GenericDataModel, SourceTable extends AppTable<DataModel>, SourceItem = AppDoc<DataModel, SourceTable>> = TableQueryFacade<DataModel, SourceTable, SourceItem> | TableRangeQueryFacade<DataModel, SourceTable, SourceItem>;
|
|
118
138
|
type TableNamespace<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = {
|
|
119
139
|
find<const Id extends GenericId<Table>>(id: Id): FindQueryBuilder<AppDoc<DataModel, Table>>;
|
|
120
140
|
findOrNull<const Id extends GenericId<Table>>(id: Id): FindOrNullQueryBuilder<AppDoc<DataModel, Table>>;
|
|
121
141
|
in<const Id extends GenericId<Table>>(ids: Id[]): TableBatchQueryFacade<DataModel, Table>;
|
|
122
|
-
|
|
142
|
+
through: {
|
|
143
|
+
<const SourceTable extends AppTable<DataModel>, SourceItem, const TargetField extends ThroughSourceField<DataModel, Table, SourceItem>>(sourceQuery: ThroughCollectionSource<DataModel, SourceTable, SourceItem>, targetField: TargetField): ThroughQueryFacade<DataModel, Table, SourceItem>;
|
|
144
|
+
<SourceItem, const TargetField extends ThroughSourceField<DataModel, Table, SourceItem>>(sourceQuery: AnyManySourceNode<SourceItem>, targetField: TargetField): ManyThroughQueryBuilder<AppDoc<DataModel, Table>, SourceItem>;
|
|
145
|
+
<SourceItem, const TargetField extends ThroughSourceField<DataModel, Table, SourceItem>>(sourceQuery: AnySingleSourceNode<SourceItem, false>, targetField: TargetField): SingleThroughQueryBuilder<AppDoc<DataModel, Table>, SourceItem, false>;
|
|
146
|
+
<SourceItem, const TargetField extends ThroughSourceField<DataModel, Table, SourceItem>>(sourceQuery: AnySingleSourceNode<SourceItem, true>, targetField: TargetField): SingleThroughQueryBuilder<AppDoc<DataModel, Table>, SourceItem, true>;
|
|
147
|
+
};
|
|
123
148
|
} & TableRangeQueryFacade<DataModel, Table> & {
|
|
124
149
|
[IndexName in TableIndexName<DataModel, Table>]: {
|
|
125
|
-
(selector: IndexSelector): TableQueryFacade<DataModel, Table>;
|
|
126
|
-
<const
|
|
150
|
+
(selector: IndexName extends UserIndex<DataModel, Table> ? IndexSelector<DataModel, Table, IndexName> : never): TableQueryFacade<DataModel, Table>;
|
|
151
|
+
<const Args extends TableIndexInvocationArgs<DataModel, Table, IndexName>>(...args: Args): TableQueryFacade<DataModel, Table>;
|
|
127
152
|
(): TableRangeQueryFacade<DataModel, Table>;
|
|
128
153
|
in<const Value extends TableIndexValueArg<DataModel, Table, IndexName>>(values: StrictTableIndexValueArg<DataModel, Table, IndexName, Value>[]): TableBatchQueryFacade<DataModel, Table>;
|
|
129
154
|
};
|
|
@@ -131,7 +156,26 @@ type TableNamespace<DataModel extends GenericDataModel, Table extends AppTable<D
|
|
|
131
156
|
type QueryFacade<DataModel extends GenericDataModel> = {
|
|
132
157
|
[Table in AppTable<DataModel>]: TableNamespace<DataModel, Table>;
|
|
133
158
|
};
|
|
159
|
+
type QuerySourcePlan = {
|
|
160
|
+
kind: 'id';
|
|
161
|
+
table: string;
|
|
162
|
+
id: GenericId<any>;
|
|
163
|
+
} | {
|
|
164
|
+
kind: 'query';
|
|
165
|
+
table: string;
|
|
166
|
+
index?: string;
|
|
167
|
+
selector?: unknown;
|
|
168
|
+
} | {
|
|
169
|
+
kind: 'batch';
|
|
170
|
+
table: string;
|
|
171
|
+
index: string;
|
|
172
|
+
values: unknown[];
|
|
173
|
+
};
|
|
174
|
+
type QueryPlan = {
|
|
175
|
+
source: QuerySourcePlan;
|
|
176
|
+
modifiers: QueryModifier[];
|
|
177
|
+
expanders: AnyWithBuilder<any, any>[];
|
|
178
|
+
};
|
|
134
179
|
declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>): QueryFacade<DataModel>;
|
|
135
|
-
declare function compute<Output = unknown>(load: () => Promise<Output> | Output): QueryNode<Output>;
|
|
136
180
|
|
|
137
|
-
export { type AppDoc, type AppTable, type QueryFacade, type RootIndexValueArg, type StrictRootIndexValueArg, type UserIndex,
|
|
181
|
+
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,13 +67,11 @@ function withExpander(plan, expander) {
|
|
|
52
67
|
expanders: [...plan.expanders, expander]
|
|
53
68
|
};
|
|
54
69
|
}
|
|
55
|
-
function withSourceKey(plan, sourceKey) {
|
|
56
|
-
return {
|
|
57
|
-
...plan,
|
|
58
|
-
sourceKey
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
70
|
function normalizeIndexValues(index, value) {
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
const fieldNames = inferFieldNamesFromIndex(index);
|
|
73
|
+
return Object.fromEntries(fieldNames.map((field, index2) => [field, value[index2]]));
|
|
74
|
+
}
|
|
62
75
|
if (isPlainObject(value)) {
|
|
63
76
|
return value;
|
|
64
77
|
}
|
|
@@ -77,8 +90,14 @@ function applyIndexValues(query, values) {
|
|
|
77
90
|
return current;
|
|
78
91
|
}
|
|
79
92
|
function inferFieldNameFromIndex(index) {
|
|
93
|
+
return inferFieldNamesFromIndex(index)[0];
|
|
94
|
+
}
|
|
95
|
+
function inferFieldNamesFromIndex(index) {
|
|
96
|
+
if (index === "by_id") {
|
|
97
|
+
return ["_id"];
|
|
98
|
+
}
|
|
80
99
|
if (index.startsWith("by") && index.length > 2) {
|
|
81
|
-
return `${
|
|
100
|
+
return index.slice(2).split("And").map((part) => `${part[0].toLowerCase()}${part.slice(1)}`);
|
|
82
101
|
}
|
|
83
102
|
throw new Error(`Cannot infer field name from index ${index}`);
|
|
84
103
|
}
|
|
@@ -101,49 +120,61 @@ function createIndexedQuery(db, table, index, selector) {
|
|
|
101
120
|
(q) => applyIndexValues(q, normalizeIndexValues(index, selector))
|
|
102
121
|
);
|
|
103
122
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
function sourceDescription(plan) {
|
|
124
|
+
switch (plan.source.kind) {
|
|
125
|
+
case "id":
|
|
126
|
+
return `${plan.source.table} with id ${plan.source.id}`;
|
|
127
|
+
case "batch":
|
|
128
|
+
return `${plan.source.table} via ${plan.source.index}`;
|
|
129
|
+
case "query":
|
|
130
|
+
return plan.source.index ? `${plan.source.table} with index ${plan.source.index}` : plan.source.table;
|
|
108
131
|
}
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
}
|
|
133
|
+
async function resolveThroughPair(db, targetField, source) {
|
|
134
|
+
const id = source[targetField];
|
|
135
|
+
if (!id) {
|
|
136
|
+
return null;
|
|
111
137
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
(q) => applyIndexValues(q, normalizeIndexValues(index, selector))
|
|
115
|
-
);
|
|
138
|
+
const target = await db.get(id);
|
|
139
|
+
return target ? { source, target } : null;
|
|
116
140
|
}
|
|
117
|
-
async function
|
|
141
|
+
async function collectThroughPairs(db, targetField, sourceItems) {
|
|
118
142
|
const pairs = await Promise.all(
|
|
119
|
-
|
|
120
|
-
const id = link[targetField];
|
|
121
|
-
const doc = id ? await db.get(id) : null;
|
|
122
|
-
return doc ? { doc, link } : null;
|
|
123
|
-
})
|
|
143
|
+
sourceItems.map(async (source) => await resolveThroughPair(db, targetField, source))
|
|
124
144
|
);
|
|
125
145
|
return pairs.filter((pair) => pair !== null);
|
|
126
146
|
}
|
|
127
|
-
async function
|
|
147
|
+
async function* iterateDecoratedSourceItems(db, plan) {
|
|
148
|
+
if (plan.source.kind !== "query") {
|
|
149
|
+
for (const item of await executeMany(db, plan)) {
|
|
150
|
+
yield item;
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const runtime = createPlanRuntime(db, plan);
|
|
155
|
+
const source = plan.source;
|
|
156
|
+
const query = buildQuery(
|
|
157
|
+
() => createIndexedQuery(db, source.table, source.index, source.selector),
|
|
158
|
+
plan.modifiers
|
|
159
|
+
);
|
|
160
|
+
for await (const rawItem of query) {
|
|
161
|
+
yield await decorateItem(plan, rawItem, await runtime.mapOne(rawItem));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function collectThroughPairsUntil(db, targetField, sourceItems, count) {
|
|
128
165
|
const pairs = [];
|
|
129
|
-
for await (const
|
|
130
|
-
const
|
|
131
|
-
if (!
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
pairs.push(
|
|
166
|
+
for await (const source of sourceItems) {
|
|
167
|
+
const pair = await resolveThroughPair(db, targetField, source);
|
|
168
|
+
if (!pair) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
pairs.push(pair);
|
|
135
172
|
if (pairs.length >= count) {
|
|
136
173
|
break;
|
|
137
174
|
}
|
|
138
175
|
}
|
|
139
176
|
return pairs;
|
|
140
177
|
}
|
|
141
|
-
function normalizeViaUniqueError(error, targetTable, joinTable, index) {
|
|
142
|
-
if (error instanceof Error && error.message === "unique() returned more than one result") {
|
|
143
|
-
return new Error(`Expected unique ${targetTable} via ${joinTable}.${index}`);
|
|
144
|
-
}
|
|
145
|
-
return error;
|
|
146
|
-
}
|
|
147
178
|
function createPlanRuntime(db, plan) {
|
|
148
179
|
const source = plan.source;
|
|
149
180
|
switch (source.kind) {
|
|
@@ -188,79 +219,23 @@ function createPlanRuntime(db, plan) {
|
|
|
188
219
|
}
|
|
189
220
|
};
|
|
190
221
|
}
|
|
191
|
-
case "via": {
|
|
192
|
-
const runQuery = () => buildQuery(
|
|
193
|
-
() => createViaQuery(db, source.joinTable, source.index, source.selector),
|
|
194
|
-
plan.modifiers
|
|
195
|
-
);
|
|
196
|
-
return {
|
|
197
|
-
unique: async () => {
|
|
198
|
-
const pairs = await collectViaPairsUntil(
|
|
199
|
-
db,
|
|
200
|
-
source.targetField,
|
|
201
|
-
runQuery(),
|
|
202
|
-
2
|
|
203
|
-
);
|
|
204
|
-
if (pairs.length > 1) {
|
|
205
|
-
throw new Error("unique() returned more than one result");
|
|
206
|
-
}
|
|
207
|
-
return pairs[0] ?? null;
|
|
208
|
-
},
|
|
209
|
-
first: async () => {
|
|
210
|
-
const pairs = await collectViaPairsUntil(
|
|
211
|
-
db,
|
|
212
|
-
source.targetField,
|
|
213
|
-
runQuery(),
|
|
214
|
-
1
|
|
215
|
-
);
|
|
216
|
-
return pairs[0] ?? null;
|
|
217
|
-
},
|
|
218
|
-
many: async () => await collectViaPairs(db, source.targetField, await runQuery().collect()),
|
|
219
|
-
take: async (count) => await collectViaPairs(db, source.targetField, await runQuery().take(count)),
|
|
220
|
-
paginate: async (opts) => {
|
|
221
|
-
const result = await runQuery().paginate(opts);
|
|
222
|
-
return {
|
|
223
|
-
page: await collectViaPairs(db, source.targetField, result.page),
|
|
224
|
-
isDone: result.isDone,
|
|
225
|
-
continueCursor: result.continueCursor
|
|
226
|
-
};
|
|
227
|
-
},
|
|
228
|
-
mapOne: async (rawItem) => rawItem.doc,
|
|
229
|
-
mapMany: async (rawItems) => rawItems.map((pair) => pair.doc),
|
|
230
|
-
missingMessages: {
|
|
231
|
-
unique: `Could not find ${source.targetTable} via ${source.joinTable}.${source.index}`,
|
|
232
|
-
first: `Could not find first ${source.targetTable} via ${source.joinTable}.${source.index}`
|
|
233
|
-
},
|
|
234
|
-
normalizeUniqueError: (error) => normalizeViaUniqueError(
|
|
235
|
-
error,
|
|
236
|
-
source.targetTable,
|
|
237
|
-
source.joinTable,
|
|
238
|
-
source.index
|
|
239
|
-
)
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
222
|
}
|
|
243
223
|
}
|
|
244
224
|
async function decorateItem(plan, rawItem, item) {
|
|
245
225
|
let output = item;
|
|
246
|
-
if (plan.source.kind === "via" && plan.sourceKey) {
|
|
247
|
-
output = { ...output, [plan.sourceKey]: rawItem.link };
|
|
248
|
-
}
|
|
249
226
|
if (plan.expanders.length > 0) {
|
|
250
|
-
output = await applyExpanders(output, plan.expanders);
|
|
227
|
+
output = await applyExpanders(output, plan.expanders, createWithContext());
|
|
251
228
|
}
|
|
252
229
|
return output;
|
|
253
230
|
}
|
|
254
231
|
async function decorateItems(plan, rawItems, items) {
|
|
255
232
|
let output = items;
|
|
256
|
-
if (plan.source.kind === "via" && plan.sourceKey) {
|
|
257
|
-
output = output.map((item, index) => ({
|
|
258
|
-
...item,
|
|
259
|
-
[plan.sourceKey]: rawItems[index].link
|
|
260
|
-
}));
|
|
261
|
-
}
|
|
262
233
|
if (plan.expanders.length > 0) {
|
|
263
|
-
output = await applyExpandersToMany(
|
|
234
|
+
output = await applyExpandersToMany(
|
|
235
|
+
output,
|
|
236
|
+
plan.expanders,
|
|
237
|
+
() => createWithContext()
|
|
238
|
+
);
|
|
264
239
|
}
|
|
265
240
|
return output;
|
|
266
241
|
}
|
|
@@ -342,10 +317,201 @@ async function executePaginate(db, plan, opts) {
|
|
|
342
317
|
continueCursor: result?.continueCursor ?? opts.cursor ?? ""
|
|
343
318
|
};
|
|
344
319
|
}
|
|
320
|
+
function createSingleQueryBuilder(executeRoot, nullable) {
|
|
321
|
+
return {
|
|
322
|
+
...createQueryNode(executeRoot),
|
|
323
|
+
_throughSourceKind: nullable ? "nullableSingle" : "single"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function createManyQueryBuilder(executeRoot) {
|
|
327
|
+
return {
|
|
328
|
+
...createQueryNode(executeRoot),
|
|
329
|
+
_throughSourceKind: "many"
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function decorateThroughItem(expanders, pair) {
|
|
333
|
+
let output = pair.target;
|
|
334
|
+
if (expanders.length > 0) {
|
|
335
|
+
output = await applyExpanders(
|
|
336
|
+
output,
|
|
337
|
+
expanders,
|
|
338
|
+
createWithContext(pair.source)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return output;
|
|
342
|
+
}
|
|
343
|
+
async function decorateThroughItems(expanders, pairs) {
|
|
344
|
+
let output = pairs.map(({ target }) => target);
|
|
345
|
+
if (expanders.length > 0) {
|
|
346
|
+
output = await applyExpandersToMany(
|
|
347
|
+
output,
|
|
348
|
+
expanders,
|
|
349
|
+
(_item, index) => createWithContext(pairs[index].source)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return output;
|
|
353
|
+
}
|
|
354
|
+
async function executeThroughCollectionMany(db, plan) {
|
|
355
|
+
const sourceItems = await executeMany(db, plan.sourcePlan);
|
|
356
|
+
const pairs = await collectThroughPairs(
|
|
357
|
+
db,
|
|
358
|
+
plan.targetField,
|
|
359
|
+
sourceItems
|
|
360
|
+
);
|
|
361
|
+
return await decorateThroughItems(plan.expanders, pairs);
|
|
362
|
+
}
|
|
363
|
+
async function executeThroughCollectionFirst(db, plan) {
|
|
364
|
+
const pair = (await collectThroughPairsUntil(
|
|
365
|
+
db,
|
|
366
|
+
plan.targetField,
|
|
367
|
+
iterateDecoratedSourceItems(db, plan.sourcePlan),
|
|
368
|
+
1
|
|
369
|
+
))[0];
|
|
370
|
+
if (!pair) {
|
|
371
|
+
throw new Error(`Could not find first ${plan.targetTable} through ${sourceDescription(plan.sourcePlan)}`);
|
|
372
|
+
}
|
|
373
|
+
return await decorateThroughItem(plan.expanders, pair);
|
|
374
|
+
}
|
|
375
|
+
async function executeThroughCollectionFirstOrNull(db, plan) {
|
|
376
|
+
const pair = (await collectThroughPairsUntil(
|
|
377
|
+
db,
|
|
378
|
+
plan.targetField,
|
|
379
|
+
iterateDecoratedSourceItems(db, plan.sourcePlan),
|
|
380
|
+
1
|
|
381
|
+
))[0];
|
|
382
|
+
return pair ? await decorateThroughItem(plan.expanders, pair) : null;
|
|
383
|
+
}
|
|
384
|
+
async function executeThroughCollectionUnique(db, plan) {
|
|
385
|
+
const pairs = await collectThroughPairsUntil(
|
|
386
|
+
db,
|
|
387
|
+
plan.targetField,
|
|
388
|
+
iterateDecoratedSourceItems(db, plan.sourcePlan),
|
|
389
|
+
2
|
|
390
|
+
);
|
|
391
|
+
if (pairs.length > 1) {
|
|
392
|
+
throw new Error("unique() returned more than one result");
|
|
393
|
+
}
|
|
394
|
+
const pair = pairs[0];
|
|
395
|
+
if (!pair) {
|
|
396
|
+
throw new Error(`Could not find ${plan.targetTable} through ${sourceDescription(plan.sourcePlan)}`);
|
|
397
|
+
}
|
|
398
|
+
return await decorateThroughItem(plan.expanders, pair);
|
|
399
|
+
}
|
|
400
|
+
async function executeThroughCollectionUniqueOrNull(db, plan) {
|
|
401
|
+
const pairs = await collectThroughPairsUntil(
|
|
402
|
+
db,
|
|
403
|
+
plan.targetField,
|
|
404
|
+
iterateDecoratedSourceItems(db, plan.sourcePlan),
|
|
405
|
+
2
|
|
406
|
+
);
|
|
407
|
+
if (pairs.length > 1) {
|
|
408
|
+
throw new Error("unique() returned more than one result");
|
|
409
|
+
}
|
|
410
|
+
return pairs[0] ? await decorateThroughItem(plan.expanders, pairs[0]) : null;
|
|
411
|
+
}
|
|
412
|
+
async function executeThroughManyNode(db, plan) {
|
|
413
|
+
const sourceItems = await plan.sourceNode._executeRoot();
|
|
414
|
+
const pairs = await collectThroughPairs(
|
|
415
|
+
db,
|
|
416
|
+
plan.targetField,
|
|
417
|
+
sourceItems
|
|
418
|
+
);
|
|
419
|
+
return await decorateThroughItems(plan.expanders, pairs);
|
|
420
|
+
}
|
|
421
|
+
async function executeThroughSingleNode(db, plan, nullable) {
|
|
422
|
+
const sourceItem = await plan.sourceNode._executeRoot();
|
|
423
|
+
if (sourceItem == null) {
|
|
424
|
+
if (nullable) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
throw new Error(`Could not find ${plan.targetTable} through source query`);
|
|
428
|
+
}
|
|
429
|
+
const pair = await resolveThroughPair(
|
|
430
|
+
db,
|
|
431
|
+
plan.targetField,
|
|
432
|
+
sourceItem
|
|
433
|
+
);
|
|
434
|
+
if (!pair) {
|
|
435
|
+
if (nullable) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
throw new Error(`Could not find ${plan.targetTable} through source query`);
|
|
439
|
+
}
|
|
440
|
+
return await decorateThroughItem(plan.expanders, pair);
|
|
441
|
+
}
|
|
442
|
+
function withThroughCollectionExpander(plan, expander) {
|
|
443
|
+
return {
|
|
444
|
+
...plan,
|
|
445
|
+
expanders: [...plan.expanders, expander]
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function withThroughNodeExpander(plan, expander) {
|
|
449
|
+
return {
|
|
450
|
+
...plan,
|
|
451
|
+
expanders: [...plan.expanders, expander]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function createThroughCollectionFacade(db, plan) {
|
|
455
|
+
return {
|
|
456
|
+
with(withBuilder) {
|
|
457
|
+
return createThroughCollectionFacade(db, withThroughCollectionExpander(plan, withBuilder));
|
|
458
|
+
},
|
|
459
|
+
unique() {
|
|
460
|
+
return createSingleQueryBuilder(
|
|
461
|
+
async () => await executeThroughCollectionUnique(db, plan),
|
|
462
|
+
false
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
uniqueOrNull() {
|
|
466
|
+
return createSingleQueryBuilder(
|
|
467
|
+
async () => await executeThroughCollectionUniqueOrNull(db, plan),
|
|
468
|
+
true
|
|
469
|
+
);
|
|
470
|
+
},
|
|
471
|
+
first() {
|
|
472
|
+
return createSingleQueryBuilder(
|
|
473
|
+
async () => await executeThroughCollectionFirst(db, plan),
|
|
474
|
+
false
|
|
475
|
+
);
|
|
476
|
+
},
|
|
477
|
+
firstOrNull() {
|
|
478
|
+
return createSingleQueryBuilder(
|
|
479
|
+
async () => await executeThroughCollectionFirstOrNull(db, plan),
|
|
480
|
+
true
|
|
481
|
+
);
|
|
482
|
+
},
|
|
483
|
+
many() {
|
|
484
|
+
return createManyQueryBuilder(
|
|
485
|
+
async () => await executeThroughCollectionMany(db, plan)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function createThroughManyQueryBuilder(db, plan) {
|
|
491
|
+
return {
|
|
492
|
+
...createQueryNode(async () => await executeThroughManyNode(db, plan)),
|
|
493
|
+
_throughSourceKind: "many",
|
|
494
|
+
with(withBuilder) {
|
|
495
|
+
return createThroughManyQueryBuilder(
|
|
496
|
+
db,
|
|
497
|
+
withThroughNodeExpander(plan, withBuilder)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function createThroughSingleQueryBuilder(db, plan, nullable) {
|
|
503
|
+
return {
|
|
504
|
+
...createQueryNode(async () => await executeThroughSingleNode(db, plan, nullable)),
|
|
505
|
+
_throughSourceKind: nullable ? "nullableSingle" : "single",
|
|
506
|
+
with(withBuilder) {
|
|
507
|
+
return createThroughSingleQueryBuilder(db, withThroughNodeExpander(plan, withBuilder), nullable);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
345
511
|
function createExpandableSingleFromPlan(db, plan, nullable) {
|
|
346
512
|
const execute = async () => nullable ? await executeFindOrNull(db, plan) : await executeFind(db, plan);
|
|
347
513
|
return {
|
|
348
|
-
...
|
|
514
|
+
...createSingleQueryBuilder(execute, nullable),
|
|
349
515
|
with(withBuilder) {
|
|
350
516
|
return createExpandableSingleFromPlan(
|
|
351
517
|
db,
|
|
@@ -364,12 +530,14 @@ function createBatchFacade(db, plan) {
|
|
|
364
530
|
);
|
|
365
531
|
},
|
|
366
532
|
many() {
|
|
367
|
-
return
|
|
533
|
+
return createManyQueryBuilder(async () => await executeMany(db, plan));
|
|
368
534
|
}
|
|
369
535
|
};
|
|
370
536
|
}
|
|
371
537
|
function createCollectionFacade(db, plan) {
|
|
372
538
|
const facade = {
|
|
539
|
+
_plan: plan,
|
|
540
|
+
_table: plan.source.table,
|
|
373
541
|
with(withBuilder) {
|
|
374
542
|
return createCollectionFacade(
|
|
375
543
|
db,
|
|
@@ -389,30 +557,33 @@ function createCollectionFacade(db, plan) {
|
|
|
389
557
|
);
|
|
390
558
|
},
|
|
391
559
|
unique() {
|
|
392
|
-
return
|
|
560
|
+
return createSingleQueryBuilder(async () => await executeUnique(db, plan), false);
|
|
393
561
|
},
|
|
394
562
|
uniqueOrNull() {
|
|
395
|
-
return
|
|
563
|
+
return createSingleQueryBuilder(
|
|
564
|
+
async () => await executeUniqueOrNull(db, plan),
|
|
565
|
+
true
|
|
566
|
+
);
|
|
396
567
|
},
|
|
397
568
|
first() {
|
|
398
|
-
return
|
|
569
|
+
return createSingleQueryBuilder(async () => await executeFirst(db, plan), false);
|
|
399
570
|
},
|
|
400
571
|
firstOrNull() {
|
|
401
|
-
return
|
|
572
|
+
return createSingleQueryBuilder(
|
|
573
|
+
async () => await executeFirstOrNull(db, plan),
|
|
574
|
+
true
|
|
575
|
+
);
|
|
402
576
|
},
|
|
403
577
|
take(count) {
|
|
404
|
-
return executeTake(db, plan, count);
|
|
578
|
+
return createManyQueryBuilder(async () => await executeTake(db, plan, count));
|
|
405
579
|
},
|
|
406
580
|
paginate(opts) {
|
|
407
581
|
return executePaginate(db, plan, opts);
|
|
408
582
|
},
|
|
409
583
|
many() {
|
|
410
|
-
return
|
|
584
|
+
return createManyQueryBuilder(async () => await executeMany(db, plan));
|
|
411
585
|
}
|
|
412
586
|
};
|
|
413
|
-
if (plan.source.kind === "via") {
|
|
414
|
-
facade.withSource = (key) => createCollectionFacade(db, withSourceKey(plan, key));
|
|
415
|
-
}
|
|
416
587
|
return facade;
|
|
417
588
|
}
|
|
418
589
|
function createIdPlan(table, id) {
|
|
@@ -438,15 +609,23 @@ function createBatchPlan(table, index, values) {
|
|
|
438
609
|
values
|
|
439
610
|
});
|
|
440
611
|
}
|
|
441
|
-
function
|
|
442
|
-
return
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
612
|
+
function isCollectionSource(value) {
|
|
613
|
+
return typeof value === "object" && value !== null && "_plan" in value && "_table" in value;
|
|
614
|
+
}
|
|
615
|
+
function isManySourceNode(value) {
|
|
616
|
+
return typeof value === "object" && value !== null && "_executeRoot" in value && value._throughSourceKind === "many";
|
|
617
|
+
}
|
|
618
|
+
function isSingleSourceNode(value) {
|
|
619
|
+
return typeof value === "object" && value !== null && "_executeRoot" in value && (value._throughSourceKind === "single" || value._throughSourceKind === "nullableSingle");
|
|
620
|
+
}
|
|
621
|
+
function normalizeIndexSelectorArgs(args) {
|
|
622
|
+
if (args.length === 0) {
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
if (args.length === 1) {
|
|
626
|
+
return args[0];
|
|
627
|
+
}
|
|
628
|
+
return args;
|
|
450
629
|
}
|
|
451
630
|
function createTableNamespace(db, table) {
|
|
452
631
|
const rootFacade = createCollectionFacade(db, createQueryPlan(table));
|
|
@@ -461,8 +640,36 @@ function createTableNamespace(db, table) {
|
|
|
461
640
|
in(ids) {
|
|
462
641
|
return createBatchFacade(db, createBatchPlan(table, "by_id", ids));
|
|
463
642
|
},
|
|
464
|
-
|
|
465
|
-
|
|
643
|
+
through(sourceQuery, targetField) {
|
|
644
|
+
if (isCollectionSource(sourceQuery)) {
|
|
645
|
+
return createThroughCollectionFacade(db, {
|
|
646
|
+
targetTable: table,
|
|
647
|
+
targetField,
|
|
648
|
+
sourcePlan: sourceQuery._plan,
|
|
649
|
+
expanders: []
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (isManySourceNode(sourceQuery)) {
|
|
653
|
+
return createThroughManyQueryBuilder(db, {
|
|
654
|
+
targetTable: table,
|
|
655
|
+
targetField,
|
|
656
|
+
sourceNode: sourceQuery,
|
|
657
|
+
expanders: []
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (isSingleSourceNode(sourceQuery)) {
|
|
661
|
+
return createThroughSingleQueryBuilder(
|
|
662
|
+
db,
|
|
663
|
+
{
|
|
664
|
+
targetTable: table,
|
|
665
|
+
targetField,
|
|
666
|
+
sourceNode: sourceQuery,
|
|
667
|
+
expanders: []
|
|
668
|
+
},
|
|
669
|
+
sourceQuery._throughSourceKind === "nullableSingle"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
throw new Error("through() requires a query facade or lazy query node");
|
|
466
673
|
}
|
|
467
674
|
};
|
|
468
675
|
return new Proxy(target, {
|
|
@@ -473,9 +680,13 @@ function createTableNamespace(db, table) {
|
|
|
473
680
|
if (RESERVED_PROMISE_KEYS.has(prop) || prop === "all") {
|
|
474
681
|
return void 0;
|
|
475
682
|
}
|
|
476
|
-
const indexMethod = ((
|
|
683
|
+
const indexMethod = ((...args) => createCollectionFacade(
|
|
477
684
|
db,
|
|
478
|
-
createQueryPlan(
|
|
685
|
+
createQueryPlan(
|
|
686
|
+
table,
|
|
687
|
+
prop,
|
|
688
|
+
normalizeIndexSelectorArgs(args)
|
|
689
|
+
)
|
|
479
690
|
));
|
|
480
691
|
indexMethod.in = (values) => createBatchFacade(
|
|
481
692
|
db,
|
|
@@ -485,28 +696,6 @@ function createTableNamespace(db, table) {
|
|
|
485
696
|
}
|
|
486
697
|
});
|
|
487
698
|
}
|
|
488
|
-
function createViaNamespace(db, targetTable, joinTable, targetField) {
|
|
489
|
-
return new Proxy(
|
|
490
|
-
{},
|
|
491
|
-
{
|
|
492
|
-
get(_target, prop) {
|
|
493
|
-
if (typeof prop !== "string" || RESERVED_PROMISE_KEYS.has(prop)) {
|
|
494
|
-
return void 0;
|
|
495
|
-
}
|
|
496
|
-
return (value) => createCollectionFacade(
|
|
497
|
-
db,
|
|
498
|
-
createViaPlan(
|
|
499
|
-
targetTable,
|
|
500
|
-
joinTable,
|
|
501
|
-
targetField,
|
|
502
|
-
prop,
|
|
503
|
-
value
|
|
504
|
-
)
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
699
|
function createQueryFacade(db) {
|
|
511
700
|
return new Proxy(
|
|
512
701
|
{},
|
|
@@ -520,9 +709,6 @@ function createQueryFacade(db) {
|
|
|
520
709
|
}
|
|
521
710
|
);
|
|
522
711
|
}
|
|
523
|
-
function compute(load) {
|
|
524
|
-
return createQueryNode(async () => await load());
|
|
525
|
-
}
|
|
526
712
|
async function queryUniqueByIndex(db, table, index, value) {
|
|
527
713
|
if (index === "by_id") {
|
|
528
714
|
return await db.query(table).withIndex("by_id", (q) => q.eq("_id", value)).unique();
|
|
@@ -533,6 +719,5 @@ async function queryUniqueByIndex(db, table, index, value) {
|
|
|
533
719
|
).unique();
|
|
534
720
|
}
|
|
535
721
|
export {
|
|
536
|
-
compute,
|
|
537
722
|
createQueryFacade
|
|
538
723
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidtkramer/convex-relations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Typed query facade helpers for Convex backends",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"convex-test": "^0.0.41",
|
|
43
43
|
"tsup": "^8.5.0",
|
|
44
44
|
"typescript": "^5.9.2",
|
|
45
|
+
"typical-data": "^0.5.0",
|
|
45
46
|
"vitest": "^4.0.15"
|
|
46
47
|
},
|
|
47
48
|
"packageManager": "pnpm@9.15.9"
|