@davidtkramer/convex-relations 0.4.0 → 0.4.2

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/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, IndexNames, NamedTableInfo, IndexRangeBuilder, NamedIndex, IndexRange, GenericTableInfo, FilterBuilder, ExpressionOrValue, GenericDatabaseReader, SchemaDefinition } from 'convex/server';
2
2
  import { GenericId } from 'convex/values';
3
3
 
4
4
  type Simplify<T> = {
@@ -139,6 +139,8 @@ type TableNamespace<DataModel extends GenericDataModel, Table extends AppTable<D
139
139
  find<const Id extends GenericId<Table>>(id: Id): FindQueryBuilder<AppDoc<DataModel, Table>>;
140
140
  findOrNull<const Id extends GenericId<Table>>(id: Id): FindOrNullQueryBuilder<AppDoc<DataModel, Table>>;
141
141
  in<const Id extends GenericId<Table>>(ids: Id[]): TableBatchQueryFacade<DataModel, Table>;
142
+ withIndex<const IndexName extends UserIndex<DataModel, Table>>(index: IndexName, selector: IndexSelector<DataModel, Table, IndexName>): TableQueryFacade<DataModel, Table>;
143
+ withIndex<const IndexName extends UserIndex<DataModel, Table>>(index: IndexName): TableRangeQueryFacade<DataModel, Table>;
142
144
  through: {
143
145
  <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
146
  <SourceItem, const TargetField extends ThroughSourceField<DataModel, Table, SourceItem>>(sourceQuery: AnyManySourceNode<SourceItem>, targetField: TargetField): ManyThroughQueryBuilder<AppDoc<DataModel, Table>, SourceItem>;
@@ -165,17 +167,19 @@ type QuerySourcePlan = {
165
167
  table: string;
166
168
  index?: string;
167
169
  selector?: unknown;
170
+ indexFields?: readonly string[];
168
171
  } | {
169
172
  kind: 'batch';
170
173
  table: string;
171
174
  index: string;
172
175
  values: unknown[];
176
+ indexFields?: readonly string[];
173
177
  };
174
178
  type QueryPlan = {
175
179
  source: QuerySourcePlan;
176
180
  modifiers: QueryModifier[];
177
181
  expanders: AnyWithBuilder<any, any>[];
178
182
  };
179
- declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>): QueryFacade<DataModel>;
183
+ declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>, schema: SchemaDefinition<any, boolean>): QueryFacade<DataModel>;
180
184
 
181
185
  export { type AppDoc, type AppTable, type QueryFacade, type RootIndexValueArg, type StrictRootIndexValueArg, type UserIndex, createQueryFacade };
package/dist/index.js CHANGED
@@ -67,16 +67,15 @@ function withExpander(plan, expander) {
67
67
  expanders: [...plan.expanders, expander]
68
68
  };
69
69
  }
70
- function normalizeIndexValues(index, value) {
70
+ function normalizeIndexValues(index, value, indexFields) {
71
71
  if (Array.isArray(value)) {
72
- const fieldNames = inferFieldNamesFromIndex(index);
73
- return Object.fromEntries(fieldNames.map((field, index2) => [field, value[index2]]));
72
+ return Object.fromEntries(indexFields.map((field, index2) => [field, value[index2]]));
74
73
  }
75
74
  if (isPlainObject(value)) {
76
75
  return value;
77
76
  }
78
77
  return {
79
- [inferFieldNameFromIndex(index)]: value
78
+ [index === "by_id" ? "_id" : indexFields[0]]: value
80
79
  };
81
80
  }
82
81
  function isPlainObject(value) {
@@ -89,19 +88,16 @@ function applyIndexValues(query, values) {
89
88
  }
90
89
  return current;
91
90
  }
92
- function inferFieldNameFromIndex(index) {
93
- return inferFieldNamesFromIndex(index)[0];
94
- }
95
- function inferFieldNamesFromIndex(index) {
91
+ function requireIndexFields(index, indexFields) {
96
92
  if (index === "by_id") {
97
93
  return ["_id"];
98
94
  }
99
- if (index.startsWith("by") && index.length > 2) {
100
- return index.slice(2).split("And").map((part) => `${part[0].toLowerCase()}${part.slice(1)}`);
95
+ if (indexFields === void 0) {
96
+ throw new Error(`Missing schema metadata for index ${index}`);
101
97
  }
102
- throw new Error(`Cannot infer field name from index ${index}`);
98
+ return indexFields;
103
99
  }
104
- function createIndexedQuery(db, table, index, selector) {
100
+ function createIndexedQuery(db, table, index, selector, indexFields = []) {
105
101
  const baseQuery = db.query(table);
106
102
  if (index === void 0) {
107
103
  return baseQuery;
@@ -115,9 +111,10 @@ function createIndexedQuery(db, table, index, selector) {
115
111
  if (index === "by_id") {
116
112
  return baseQuery.withIndex(index, (q) => q.eq("_id", selector));
117
113
  }
114
+ const fields = requireIndexFields(index, indexFields);
118
115
  return baseQuery.withIndex(
119
116
  index,
120
- (q) => applyIndexValues(q, normalizeIndexValues(index, selector))
117
+ (q) => applyIndexValues(q, normalizeIndexValues(index, selector, fields))
121
118
  );
122
119
  }
123
120
  function sourceDescription(plan) {
@@ -154,7 +151,13 @@ async function* iterateDecoratedSourceItems(db, plan) {
154
151
  const runtime = createPlanRuntime(db, plan);
155
152
  const source = plan.source;
156
153
  const query = buildQuery(
157
- () => createIndexedQuery(db, source.table, source.index, source.selector),
154
+ () => createIndexedQuery(
155
+ db,
156
+ source.table,
157
+ source.index,
158
+ source.selector,
159
+ source.indexFields
160
+ ),
158
161
  plan.modifiers
159
162
  );
160
163
  for await (const rawItem of query) {
@@ -191,18 +194,28 @@ function createPlanRuntime(db, plan) {
191
194
  return {
192
195
  many: async () => (await Promise.all(
193
196
  source.values.map(
194
- async (value) => await queryUniqueByIndex(db, source.table, source.index, value)
197
+ async (value) => await queryManyByIndex(
198
+ db,
199
+ source.table,
200
+ source.index,
201
+ value,
202
+ source.indexFields
203
+ )
195
204
  )
196
- )).filter(
197
- (doc) => doc !== null
198
- ),
205
+ )).flat(),
199
206
  mapOne: async (rawItem) => rawItem,
200
207
  mapMany: async (rawItems) => rawItems,
201
208
  missingMessages: {}
202
209
  };
203
210
  case "query": {
204
211
  const runQuery = () => buildQuery(
205
- () => createIndexedQuery(db, source.table, source.index, source.selector),
212
+ () => createIndexedQuery(
213
+ db,
214
+ source.table,
215
+ source.index,
216
+ source.selector,
217
+ source.indexFields
218
+ ),
206
219
  plan.modifiers
207
220
  );
208
221
  return {
@@ -593,20 +606,22 @@ function createIdPlan(table, id) {
593
606
  id
594
607
  });
595
608
  }
596
- function createQueryPlan(table, index, selector) {
609
+ function createQueryPlan(table, index, selector, indexFields) {
597
610
  return createPlan({
598
611
  kind: "query",
599
612
  table,
600
613
  index,
601
- selector
614
+ selector,
615
+ indexFields
602
616
  });
603
617
  }
604
- function createBatchPlan(table, index, values) {
618
+ function createBatchPlan(table, index, values, indexFields) {
605
619
  return createPlan({
606
620
  kind: "batch",
607
621
  table,
608
622
  index,
609
- values
623
+ values,
624
+ indexFields
610
625
  });
611
626
  }
612
627
  function isCollectionSource(value) {
@@ -627,7 +642,7 @@ function normalizeIndexSelectorArgs(args) {
627
642
  }
628
643
  return args;
629
644
  }
630
- function createTableNamespace(db, table) {
645
+ function createTableNamespace(db, table, resolveIndexFields) {
631
646
  const rootFacade = createCollectionFacade(db, createQueryPlan(table));
632
647
  const target = {
633
648
  ...rootFacade,
@@ -640,6 +655,12 @@ function createTableNamespace(db, table) {
640
655
  in(ids) {
641
656
  return createBatchFacade(db, createBatchPlan(table, "by_id", ids));
642
657
  },
658
+ withIndex(index, selector) {
659
+ return createCollectionFacade(
660
+ db,
661
+ createQueryPlan(table, index, selector, resolveIndexFields(table, index))
662
+ );
663
+ },
643
664
  through(sourceQuery, targetField) {
644
665
  if (isCollectionSource(sourceQuery)) {
645
666
  return createThroughCollectionFacade(db, {
@@ -685,18 +706,49 @@ function createTableNamespace(db, table) {
685
706
  createQueryPlan(
686
707
  table,
687
708
  prop,
688
- normalizeIndexSelectorArgs(args)
709
+ normalizeIndexSelectorArgs(args),
710
+ resolveIndexFields(table, prop)
689
711
  )
690
712
  ));
691
713
  indexMethod.in = (values) => createBatchFacade(
692
714
  db,
693
- createBatchPlan(table, prop, values)
715
+ createBatchPlan(
716
+ table,
717
+ prop,
718
+ values,
719
+ resolveIndexFields(table, prop)
720
+ )
694
721
  );
695
722
  return indexMethod;
696
723
  }
697
724
  });
698
725
  }
699
- function createQueryFacade(db) {
726
+ function createIndexFieldResolver(schema) {
727
+ const cache = /* @__PURE__ */ new Map();
728
+ return (table, index) => {
729
+ if (index === "by_id") {
730
+ return ["_id"];
731
+ }
732
+ const cacheKey = `${table}:${index}`;
733
+ const cached = cache.get(cacheKey);
734
+ if (cached !== void 0) {
735
+ return cached;
736
+ }
737
+ const tableDefinition = schema.tables[table];
738
+ const indexes = tableDefinition?.[" indexes"]();
739
+ const match = indexes?.find(
740
+ (candidate) => candidate.indexDescriptor === index
741
+ );
742
+ if (match !== void 0 && Array.isArray(match.fields)) {
743
+ const fields = [...match.fields];
744
+ cache.set(cacheKey, fields);
745
+ return fields;
746
+ }
747
+ throw new Error(`Missing schema metadata for index ${table}.${index}`);
748
+ };
749
+ }
750
+ function createQueryFacade(db, schema) {
751
+ const resolveIndexFields = createIndexFieldResolver(schema);
700
752
  return new Proxy(
701
753
  {},
702
754
  {
@@ -704,19 +756,25 @@ function createQueryFacade(db) {
704
756
  if (typeof prop !== "string" || RESERVED_PROMISE_KEYS.has(prop)) {
705
757
  return void 0;
706
758
  }
707
- return createTableNamespace(db, prop);
759
+ return createTableNamespace(
760
+ db,
761
+ prop,
762
+ resolveIndexFields
763
+ );
708
764
  }
709
765
  }
710
766
  );
711
767
  }
712
- async function queryUniqueByIndex(db, table, index, value) {
768
+ async function queryManyByIndex(db, table, index, value, indexFields) {
713
769
  if (index === "by_id") {
714
- return await db.query(table).withIndex("by_id", (q) => q.eq("_id", value)).unique();
770
+ const doc = await db.query(table).withIndex("by_id", (q) => q.eq("_id", value)).unique();
771
+ return doc ? [doc] : [];
715
772
  }
773
+ const fields = requireIndexFields(index, indexFields);
716
774
  return await db.query(table).withIndex(
717
775
  index,
718
- (q) => applyIndexValues(q, normalizeIndexValues(index, value))
719
- ).unique();
776
+ (q) => applyIndexValues(q, normalizeIndexValues(index, value, fields))
777
+ ).collect();
720
778
  }
721
779
  export {
722
780
  createQueryFacade
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidtkramer/convex-relations",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Typed query facade helpers for Convex backends",
5
5
  "type": "module",
6
6
  "license": "MIT",