@davidtkramer/convex-relations 0.4.2 → 0.5.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)
@@ -513,6 +514,43 @@ const page = await q.posts.byAuthorId(authorId).paginate({
513
514
  });
514
515
  ```
515
516
 
517
+ ## In-Memory Shaping
518
+
519
+ `many()` and `take(count)` return lazy nodes. Chain `filter(...)` or `sort(...)`
520
+ on them to shape the loaded rows in memory before the node resolves. Both run
521
+ after `with(...)` expansions, so they can read expanded relations, which the
522
+ database-side `filter(...)` on a range query cannot.
523
+
524
+ ```ts
525
+ const comments = await q.comments
526
+ .byPostId(postId)
527
+ .with((comment) => ({ author: q.authors.findOrNull(comment.authorId) }))
528
+ .many()
529
+ .filter((comment): comment is typeof comment & { author: Author } =>
530
+ comment.author !== null,
531
+ )
532
+ .sort((a, b) => b.author.reputation - a.author.reputation);
533
+ ```
534
+
535
+ A type-predicate `filter(...)` narrows the result type. `sort(...)` requires a
536
+ comparator and never mutates the loaded array.
537
+
538
+ The shaped node is still a lazy node: it works as a `with(...)` value and as a
539
+ `through(...)` source.
540
+
541
+ ```ts
542
+ const post = await q.posts.find(postId).with((post) => ({
543
+ approvedComments: q.comments
544
+ .byPostId(post._id)
545
+ .many()
546
+ .filter((comment) => comment.status === "approved"),
547
+ }));
548
+ ```
549
+
550
+ Prefer the database-side `filter(...)` and `order(...)` on the range query when
551
+ the condition only involves the row's own fields; in-memory shaping still loads
552
+ and expands every row before discarding it.
553
+
516
554
  ## Error Semantics
517
555
 
518
556
  - `find(...)` throws if the document is missing
package/dist/index.d.ts CHANGED
@@ -80,7 +80,11 @@ type FirstQueryBuilder<Item> = SingleQueryBuilder<Item, false>;
80
80
  type FirstOrNullQueryBuilder<Item> = SingleQueryBuilder<Item, true>;
81
81
  type FindQueryBuilder<Item> = ExpandableSingleQueryBuilder<Item, false>;
82
82
  type FindOrNullQueryBuilder<Item> = ExpandableSingleQueryBuilder<Item, true>;
83
- type ManyQueryBuilder<Item> = QueryNode<Item[]> & ThroughNodeHandle<Item, 'many'>;
83
+ type ManyQueryBuilder<Item> = QueryNode<Item[]> & ThroughNodeHandle<Item, 'many'> & {
84
+ filter<Narrowed extends Item>(predicate: (item: Item, index: number) => item is Narrowed): ManyQueryBuilder<Narrowed>;
85
+ filter(predicate: (item: Item, index: number) => unknown): ManyQueryBuilder<Item>;
86
+ sort(compare: (a: Item, b: Item) => number): ManyQueryBuilder<Item>;
87
+ };
84
88
  type BatchQueryBuilder<Item> = ManyQueryBuilder<Item>;
85
89
  type ThroughSourceNodeKind = 'many' | 'single' | 'nullableSingle';
86
90
  type ThroughNodeHandle<SourceItem, Kind extends ThroughSourceNodeKind> = {
package/dist/index.js CHANGED
@@ -69,7 +69,9 @@ function withExpander(plan, expander) {
69
69
  }
70
70
  function normalizeIndexValues(index, value, indexFields) {
71
71
  if (Array.isArray(value)) {
72
- return Object.fromEntries(indexFields.map((field, index2) => [field, value[index2]]));
72
+ return Object.fromEntries(
73
+ indexFields.slice(0, value.length).map((field, index2) => [field, value[index2]])
74
+ );
73
75
  }
74
76
  if (isPlainObject(value)) {
75
77
  return value;
@@ -339,7 +341,15 @@ function createSingleQueryBuilder(executeRoot, nullable) {
339
341
  function createManyQueryBuilder(executeRoot) {
340
342
  return {
341
343
  ...createQueryNode(executeRoot),
342
- _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
+ }
343
353
  };
344
354
  }
345
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.2",
3
+ "version": "0.5.0",
4
4
  "description": "Typed query facade helpers for Convex backends",
5
5
  "type": "module",
6
6
  "license": "MIT",