@davidtkramer/convex-relations 0.5.0 → 0.6.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 +46 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.js +18 -1
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -345,6 +345,42 @@ const exactOrPrefix = await q.comments
|
|
|
345
345
|
.many();
|
|
346
346
|
```
|
|
347
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
|
+
|
|
348
384
|
### Indexed lookup by selector function
|
|
349
385
|
|
|
350
386
|
You can also pass Convex's index selector callback:
|
|
@@ -365,9 +401,18 @@ const posts = await q.posts.in(postIds).many();
|
|
|
365
401
|
const categories = await q.categories.bySlug
|
|
366
402
|
.in(["typescript", "convex"])
|
|
367
403
|
.many();
|
|
404
|
+
|
|
405
|
+
// Compound indexes take positional prefixes, one tuple per lookup.
|
|
406
|
+
const comments = await q.comments.byPostIdAndStatus
|
|
407
|
+
.in([
|
|
408
|
+
[postId, "approved"],
|
|
409
|
+
[otherPostId, "pending"],
|
|
410
|
+
])
|
|
411
|
+
.many();
|
|
368
412
|
```
|
|
369
413
|
|
|
370
|
-
|
|
414
|
+
Each value runs as its own index query, in parallel; results are concatenated
|
|
415
|
+
in list order. Batch lookups skip missing rows and dedupe the values.
|
|
371
416
|
|
|
372
417
|
## Relation Expansion with `with(...)`
|
|
373
418
|
|
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>
|
|
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>
|
|
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
|
|
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
|
|
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;
|
|
@@ -154,9 +162,9 @@ type TableNamespace<DataModel extends GenericDataModel, Table extends AppTable<D
|
|
|
154
162
|
} & TableRangeQueryFacade<DataModel, Table> & {
|
|
155
163
|
[IndexName in TableIndexName<DataModel, Table>]: {
|
|
156
164
|
(selector: IndexName extends UserIndex<DataModel, Table> ? IndexSelector<DataModel, Table, IndexName> : never): TableQueryFacade<DataModel, Table>;
|
|
157
|
-
<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>>;
|
|
158
166
|
(): TableRangeQueryFacade<DataModel, Table>;
|
|
159
|
-
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]>>;
|
|
160
168
|
};
|
|
161
169
|
};
|
|
162
170
|
type QueryFacade<DataModel extends GenericDataModel> = {
|
package/dist/index.js
CHANGED
|
@@ -630,10 +630,27 @@ function createBatchPlan(table, index, values, indexFields) {
|
|
|
630
630
|
kind: "batch",
|
|
631
631
|
table,
|
|
632
632
|
index,
|
|
633
|
-
values,
|
|
633
|
+
values: uniqueBatchValues(values),
|
|
634
634
|
indexFields
|
|
635
635
|
});
|
|
636
636
|
}
|
|
637
|
+
function uniqueBatchValues(values) {
|
|
638
|
+
const seenScalars = /* @__PURE__ */ new Set();
|
|
639
|
+
const seenTuples = [];
|
|
640
|
+
return values.filter((value) => {
|
|
641
|
+
if (!Array.isArray(value)) {
|
|
642
|
+
if (seenScalars.has(value)) return false;
|
|
643
|
+
seenScalars.add(value);
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
if (seenTuples.some((other) => sameBatchValue(other, value))) return false;
|
|
647
|
+
seenTuples.push(value);
|
|
648
|
+
return true;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function sameBatchValue(a, b) {
|
|
652
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => Object.is(item, b[index]));
|
|
653
|
+
}
|
|
637
654
|
function isCollectionSource(value) {
|
|
638
655
|
return typeof value === "object" && value !== null && "_plan" in value && "_table" in value;
|
|
639
656
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidtkramer/convex-relations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Typed query facade helpers for Convex backends",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,11 +28,6 @@
|
|
|
28
28
|
},
|
|
29
29
|
"main": "./dist/index.js",
|
|
30
30
|
"types": "./dist/index.d.ts",
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
33
|
-
"check-types": "tsc --noEmit",
|
|
34
|
-
"test": "vitest run --typecheck"
|
|
35
|
-
},
|
|
36
31
|
"peerDependencies": {
|
|
37
32
|
"convex": "^1.25.4"
|
|
38
33
|
},
|
|
@@ -45,5 +40,9 @@
|
|
|
45
40
|
"typical-data": "^0.5.0",
|
|
46
41
|
"vitest": "^4.0.15"
|
|
47
42
|
},
|
|
48
|
-
"
|
|
49
|
-
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
45
|
+
"check-types": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run --typecheck"
|
|
47
|
+
}
|
|
48
|
+
}
|