@davidtkramer/convex-relations 0.4.0 → 0.4.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/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, GenericTableInfo, FilterBuilder, ExpressionOrValue, NamedTableInfo, IndexNames, IndexRangeBuilder, NamedIndex, IndexRange, GenericDatabaseReader, SchemaDefinition } from 'convex/server';
2
2
  import { GenericId } from 'convex/values';
3
3
 
4
4
  type Simplify<T> = {
@@ -165,17 +165,19 @@ type QuerySourcePlan = {
165
165
  table: string;
166
166
  index?: string;
167
167
  selector?: unknown;
168
+ indexFields?: readonly string[];
168
169
  } | {
169
170
  kind: 'batch';
170
171
  table: string;
171
172
  index: string;
172
173
  values: unknown[];
174
+ indexFields?: readonly string[];
173
175
  };
174
176
  type QueryPlan = {
175
177
  source: QuerySourcePlan;
176
178
  modifiers: QueryModifier[];
177
179
  expanders: AnyWithBuilder<any, any>[];
178
180
  };
179
- declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>): QueryFacade<DataModel>;
181
+ declare function createQueryFacade<DataModel extends GenericDataModel>(db: GenericDatabaseReader<DataModel>, schema: SchemaDefinition<any, boolean>): QueryFacade<DataModel>;
180
182
 
181
183
  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,
@@ -685,18 +700,49 @@ function createTableNamespace(db, table) {
685
700
  createQueryPlan(
686
701
  table,
687
702
  prop,
688
- normalizeIndexSelectorArgs(args)
703
+ normalizeIndexSelectorArgs(args),
704
+ resolveIndexFields(table, prop)
689
705
  )
690
706
  ));
691
707
  indexMethod.in = (values) => createBatchFacade(
692
708
  db,
693
- createBatchPlan(table, prop, values)
709
+ createBatchPlan(
710
+ table,
711
+ prop,
712
+ values,
713
+ resolveIndexFields(table, prop)
714
+ )
694
715
  );
695
716
  return indexMethod;
696
717
  }
697
718
  });
698
719
  }
699
- function createQueryFacade(db) {
720
+ function createIndexFieldResolver(schema) {
721
+ const cache = /* @__PURE__ */ new Map();
722
+ return (table, index) => {
723
+ if (index === "by_id") {
724
+ return ["_id"];
725
+ }
726
+ const cacheKey = `${table}:${index}`;
727
+ const cached = cache.get(cacheKey);
728
+ if (cached !== void 0) {
729
+ return cached;
730
+ }
731
+ const tableDefinition = schema.tables[table];
732
+ const indexes = tableDefinition?.[" indexes"]();
733
+ const match = indexes?.find(
734
+ (candidate) => candidate.indexDescriptor === index
735
+ );
736
+ if (match !== void 0 && Array.isArray(match.fields)) {
737
+ const fields = [...match.fields];
738
+ cache.set(cacheKey, fields);
739
+ return fields;
740
+ }
741
+ throw new Error(`Missing schema metadata for index ${table}.${index}`);
742
+ };
743
+ }
744
+ function createQueryFacade(db, schema) {
745
+ const resolveIndexFields = createIndexFieldResolver(schema);
700
746
  return new Proxy(
701
747
  {},
702
748
  {
@@ -704,19 +750,25 @@ function createQueryFacade(db) {
704
750
  if (typeof prop !== "string" || RESERVED_PROMISE_KEYS.has(prop)) {
705
751
  return void 0;
706
752
  }
707
- return createTableNamespace(db, prop);
753
+ return createTableNamespace(
754
+ db,
755
+ prop,
756
+ resolveIndexFields
757
+ );
708
758
  }
709
759
  }
710
760
  );
711
761
  }
712
- async function queryUniqueByIndex(db, table, index, value) {
762
+ async function queryManyByIndex(db, table, index, value, indexFields) {
713
763
  if (index === "by_id") {
714
- return await db.query(table).withIndex("by_id", (q) => q.eq("_id", value)).unique();
764
+ const doc = await db.query(table).withIndex("by_id", (q) => q.eq("_id", value)).unique();
765
+ return doc ? [doc] : [];
715
766
  }
767
+ const fields = requireIndexFields(index, indexFields);
716
768
  return await db.query(table).withIndex(
717
769
  index,
718
- (q) => applyIndexValues(q, normalizeIndexValues(index, value))
719
- ).unique();
770
+ (q) => applyIndexValues(q, normalizeIndexValues(index, value, fields))
771
+ ).collect();
720
772
  }
721
773
  export {
722
774
  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.1",
4
4
  "description": "Typed query facade helpers for Convex backends",
5
5
  "type": "module",
6
6
  "license": "MIT",