@davidtkramer/convex-relations 0.4.3 → 0.6.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 CHANGED
@@ -123,6 +123,7 @@ That works, but you are responsible for:
123
123
  - [Relation Expansion with `with(...)`](#relation-expansion-with-with)
124
124
  - [Reference Traversal with `through(...)`](#reference-traversal-with-through)
125
125
  - [Terminals](#terminals)
126
+ - [In-Memory Shaping](#in-memory-shaping)
126
127
  - [Error Semantics](#error-semantics)
127
128
  - [Performance Characteristics](#performance-characteristics)
128
129
  - [Comparison to `convex-helpers/server/relationships`](#comparison-to-convex-helpersserverrelationships)
@@ -344,6 +345,42 @@ const exactOrPrefix = await q.comments
344
345
  .many();
345
346
  ```
346
347
 
348
+ ### Union tables narrow by index
349
+
350
+ When a table's document is a `v.union(...)`, a positional lookup narrows the
351
+ result to the variants an index equality can actually return. A variant is
352
+ kept when its field can hold the value, or when it lacks the field and the
353
+ value may be `undefined` (Convex indexes a missing field as `undefined`).
354
+
355
+ ```ts
356
+ const activities = defineTable(
357
+ v.union(
358
+ v.object({ postId: v.id("posts"), kind: v.literal("view") }),
359
+ v.object({ postId: v.id("posts"), kind: v.literal("share"), channel: v.string() }),
360
+ v.object({
361
+ postId: v.id("posts"),
362
+ kind: v.union(v.literal("flag"), v.literal("report")),
363
+ status: v.union(v.literal("pending"), v.literal("resolved")),
364
+ }),
365
+ ),
366
+ )
367
+ .index("byKind", ["kind"])
368
+ .index("byPostIdAndStatus", ["postId", "status"]);
369
+
370
+ // { kind: "share"; channel: string; ... }[]
371
+ const shares = await q.activities.byKind("share").many();
372
+
373
+ // Only the moderation variant carries `status`, so this narrows too.
374
+ const pending = await q.activities.byPostIdAndStatus(postId, "pending").many();
375
+
376
+ // `.in(...)` narrows on the union of its values.
377
+ const taps = await q.activities.byKind.in(["view", "share"]).many();
378
+ ```
379
+
380
+ A prefix that stops before the discriminating field, the selector-function
381
+ form, and `.filter(...)` all return the full union. Tables that are not unions
382
+ are unaffected.
383
+
347
384
  ### Indexed lookup by selector function
348
385
 
349
386
  You can also pass Convex's index selector callback:
@@ -513,6 +550,43 @@ const page = await q.posts.byAuthorId(authorId).paginate({
513
550
  });
514
551
  ```
515
552
 
553
+ ## In-Memory Shaping
554
+
555
+ `many()` and `take(count)` return lazy nodes. Chain `filter(...)` or `sort(...)`
556
+ on them to shape the loaded rows in memory before the node resolves. Both run
557
+ after `with(...)` expansions, so they can read expanded relations, which the
558
+ database-side `filter(...)` on a range query cannot.
559
+
560
+ ```ts
561
+ const comments = await q.comments
562
+ .byPostId(postId)
563
+ .with((comment) => ({ author: q.authors.findOrNull(comment.authorId) }))
564
+ .many()
565
+ .filter((comment): comment is typeof comment & { author: Author } =>
566
+ comment.author !== null,
567
+ )
568
+ .sort((a, b) => b.author.reputation - a.author.reputation);
569
+ ```
570
+
571
+ A type-predicate `filter(...)` narrows the result type. `sort(...)` requires a
572
+ comparator and never mutates the loaded array.
573
+
574
+ The shaped node is still a lazy node: it works as a `with(...)` value and as a
575
+ `through(...)` source.
576
+
577
+ ```ts
578
+ const post = await q.posts.find(postId).with((post) => ({
579
+ approvedComments: q.comments
580
+ .byPostId(post._id)
581
+ .many()
582
+ .filter((comment) => comment.status === "approved"),
583
+ }));
584
+ ```
585
+
586
+ Prefer the database-side `filter(...)` and `order(...)` on the range query when
587
+ the condition only involves the row's own fields; in-memory shaping still loads
588
+ and expands every row before discarding it.
589
+
516
590
  ## Error Semantics
517
591
 
518
592
  - `find(...)` throws if the document is missing
package/dist/index.d.ts CHANGED
@@ -16,21 +16,29 @@ type UserIndexFields<DataModel extends GenericDataModel, Table extends AppTable<
16
16
  type SingleIndexField<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = UserIndexFields<DataModel, Table, IndexName> extends readonly [
17
17
  infer Field extends string
18
18
  ] ? Field : never;
19
+ type DocFieldType<Doc, Field extends string> = Doc extends unknown ? Field extends keyof Doc ? Doc[Field] : never : never;
20
+ type Overlaps<A, B> = [Extract<A, B> | Extract<B, A>] extends [never] ? false : true;
21
+ type VariantsMatching<Doc, Field extends string, Value> = Doc extends unknown ? Field extends keyof Doc ? Overlaps<Doc[Field], Value> extends true ? Doc : never : undefined extends Value ? Doc : never : never;
22
+ type NarrowByIndexArgs<Doc, Fields extends readonly string[], Args extends readonly unknown[]> = Args extends readonly [infer Value, ...infer RestArgs] ? Fields extends readonly [
23
+ infer Field extends string,
24
+ ...infer RestFields extends readonly string[]
25
+ ] ? NarrowByIndexArgs<VariantsMatching<Doc, Field, Value>, RestFields, RestArgs> : Doc : Doc;
26
+ type NarrowedIndexItem<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends TableIndexName<DataModel, Table>, Args extends readonly unknown[]> = IndexName extends UserIndex<DataModel, Table> ? NarrowByIndexArgs<AppDoc<DataModel, Table>, UserIndexFields<DataModel, Table, IndexName>, Args> : AppDoc<DataModel, Table>;
19
27
  type TuplePrefixArgs<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, Fields extends readonly string[], Seen extends readonly string[] = [], SeenValues extends readonly unknown[] = []> = Fields extends readonly [
20
28
  infer Head extends string,
21
29
  ...infer Tail extends readonly string[]
22
- ] ? [...SeenValues, AppDoc<DataModel, Table>[Head]] | TuplePrefixArgs<DataModel, Table, Tail, [
30
+ ] ? [...SeenValues, DocFieldType<AppDoc<DataModel, Table>, Head>] | TuplePrefixArgs<DataModel, Table, Tail, [
23
31
  ...Seen,
24
32
  Head
25
33
  ], [
26
34
  ...SeenValues,
27
- AppDoc<DataModel, Table>[Head]
35
+ DocFieldType<AppDoc<DataModel, Table>, Head>
28
36
  ]> : never;
29
37
  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>]);
38
+ 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 : DocFieldType<AppDoc<DataModel, Table>, SingleIndexField<DataModel, Table, IndexName>>);
31
39
  type StrictRootIndexValueArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>, Value extends RootIndexValueArg<DataModel, Table, IndexName>> = Value;
32
40
  type TableIndexName<DataModel extends GenericDataModel, Table extends AppTable<DataModel>> = UserIndex<DataModel, Table> | 'by_id';
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>];
41
+ type UserIndexArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends UserIndex<DataModel, Table>> = SingleIndexField<DataModel, Table, IndexName> extends never ? PositionalIndexArgs<DataModel, Table, IndexName> : DocFieldType<AppDoc<DataModel, Table>, SingleIndexField<DataModel, Table, IndexName>>;
34
42
  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
43
  type StrictTableIndexValueArg<DataModel extends GenericDataModel, Table extends AppTable<DataModel>, IndexName extends TableIndexName<DataModel, Table>, Value extends TableIndexValueArg<DataModel, Table, IndexName>> = Value;
36
44
  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;
@@ -80,7 +88,11 @@ type FirstQueryBuilder<Item> = SingleQueryBuilder<Item, false>;
80
88
  type FirstOrNullQueryBuilder<Item> = SingleQueryBuilder<Item, true>;
81
89
  type FindQueryBuilder<Item> = ExpandableSingleQueryBuilder<Item, false>;
82
90
  type FindOrNullQueryBuilder<Item> = ExpandableSingleQueryBuilder<Item, true>;
83
- type ManyQueryBuilder<Item> = QueryNode<Item[]> & ThroughNodeHandle<Item, 'many'>;
91
+ type ManyQueryBuilder<Item> = QueryNode<Item[]> & ThroughNodeHandle<Item, 'many'> & {
92
+ filter<Narrowed extends Item>(predicate: (item: Item, index: number) => item is Narrowed): ManyQueryBuilder<Narrowed>;
93
+ filter(predicate: (item: Item, index: number) => unknown): ManyQueryBuilder<Item>;
94
+ sort(compare: (a: Item, b: Item) => number): ManyQueryBuilder<Item>;
95
+ };
84
96
  type BatchQueryBuilder<Item> = ManyQueryBuilder<Item>;
85
97
  type ThroughSourceNodeKind = 'many' | 'single' | 'nullableSingle';
86
98
  type ThroughNodeHandle<SourceItem, Kind extends ThroughSourceNodeKind> = {
@@ -150,9 +162,9 @@ type TableNamespace<DataModel extends GenericDataModel, Table extends AppTable<D
150
162
  } & TableRangeQueryFacade<DataModel, Table> & {
151
163
  [IndexName in TableIndexName<DataModel, Table>]: {
152
164
  (selector: IndexName extends UserIndex<DataModel, Table> ? IndexSelector<DataModel, Table, IndexName> : never): TableQueryFacade<DataModel, Table>;
153
- <const Args extends TableIndexInvocationArgs<DataModel, Table, IndexName>>(...args: Args): TableQueryFacade<DataModel, Table>;
165
+ <const Args extends TableIndexInvocationArgs<DataModel, Table, IndexName>>(...args: Args): TableQueryFacade<DataModel, Table, NarrowedIndexItem<DataModel, Table, IndexName, Args>>;
154
166
  (): TableRangeQueryFacade<DataModel, Table>;
155
- in<const Value extends TableIndexValueArg<DataModel, Table, IndexName>>(values: StrictTableIndexValueArg<DataModel, Table, IndexName, Value>[]): TableBatchQueryFacade<DataModel, Table>;
167
+ in<const Value extends TableIndexValueArg<DataModel, Table, IndexName>>(values: StrictTableIndexValueArg<DataModel, Table, IndexName, Value>[]): TableBatchQueryFacade<DataModel, Table, NarrowedIndexItem<DataModel, Table, IndexName, Value extends readonly unknown[] ? Value : [Value]>>;
156
168
  };
157
169
  };
158
170
  type QueryFacade<DataModel extends GenericDataModel> = {
package/dist/index.js CHANGED
@@ -341,7 +341,15 @@ function createSingleQueryBuilder(executeRoot, nullable) {
341
341
  function createManyQueryBuilder(executeRoot) {
342
342
  return {
343
343
  ...createQueryNode(executeRoot),
344
- _throughSourceKind: "many"
344
+ _throughSourceKind: "many",
345
+ filter(predicate) {
346
+ return createManyQueryBuilder(
347
+ async () => (await executeRoot()).filter((item, index) => predicate(item, index))
348
+ );
349
+ },
350
+ sort(compare) {
351
+ return createManyQueryBuilder(async () => [...await executeRoot()].sort(compare));
352
+ }
345
353
  };
346
354
  }
347
355
  async function decorateThroughItem(expanders, pair) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidtkramer/convex-relations",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "Typed query facade helpers for Convex backends",
5
5
  "type": "module",
6
6
  "license": "MIT",