@neutralauth/internal-auth 0.10.11

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.
Files changed (147) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/auth-config.d.ts +43 -0
  4. package/dist/auth-config.d.ts.map +1 -0
  5. package/dist/auth-config.js +43 -0
  6. package/dist/auth-config.js.map +1 -0
  7. package/dist/auth-options.d.ts +3 -0
  8. package/dist/auth-options.d.ts.map +1 -0
  9. package/dist/auth-options.js +40 -0
  10. package/dist/auth-options.js.map +1 -0
  11. package/dist/auth.d.ts +2 -0
  12. package/dist/auth.d.ts.map +1 -0
  13. package/dist/auth.js +4 -0
  14. package/dist/auth.js.map +1 -0
  15. package/dist/client/adapter-utils.d.ts +66 -0
  16. package/dist/client/adapter-utils.d.ts.map +1 -0
  17. package/dist/client/adapter-utils.js +437 -0
  18. package/dist/client/adapter-utils.js.map +1 -0
  19. package/dist/client/adapter.d.ts +14 -0
  20. package/dist/client/adapter.d.ts.map +1 -0
  21. package/dist/client/adapter.js +274 -0
  22. package/dist/client/adapter.js.map +1 -0
  23. package/dist/client/create-api.d.ts +141 -0
  24. package/dist/client/create-api.d.ts.map +1 -0
  25. package/dist/client/create-api.js +205 -0
  26. package/dist/client/create-api.js.map +1 -0
  27. package/dist/client/create-client.d.ts +183 -0
  28. package/dist/client/create-client.d.ts.map +1 -0
  29. package/dist/client/create-client.js +311 -0
  30. package/dist/client/create-client.js.map +1 -0
  31. package/dist/client/create-schema.d.ts +19 -0
  32. package/dist/client/create-schema.d.ts.map +1 -0
  33. package/dist/client/create-schema.js +114 -0
  34. package/dist/client/create-schema.js.map +1 -0
  35. package/dist/client/index.d.ts +7 -0
  36. package/dist/client/index.d.ts.map +1 -0
  37. package/dist/client/index.js +10 -0
  38. package/dist/client/index.js.map +1 -0
  39. package/dist/client/plugins/index.d.ts +3 -0
  40. package/dist/client/plugins/index.d.ts.map +1 -0
  41. package/dist/client/plugins/index.js +3 -0
  42. package/dist/client/plugins/index.js.map +1 -0
  43. package/dist/component/_generated/api.d.ts +36 -0
  44. package/dist/component/_generated/api.d.ts.map +1 -0
  45. package/dist/component/_generated/api.js +31 -0
  46. package/dist/component/_generated/api.js.map +1 -0
  47. package/dist/component/_generated/component.d.ts +787 -0
  48. package/dist/component/_generated/component.d.ts.map +1 -0
  49. package/dist/component/_generated/component.js +11 -0
  50. package/dist/component/_generated/component.js.map +1 -0
  51. package/dist/component/_generated/dataModel.d.ts +46 -0
  52. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  53. package/dist/component/_generated/dataModel.js +11 -0
  54. package/dist/component/_generated/dataModel.js.map +1 -0
  55. package/dist/component/_generated/server.d.ts +121 -0
  56. package/dist/component/_generated/server.d.ts.map +1 -0
  57. package/dist/component/_generated/server.js +78 -0
  58. package/dist/component/_generated/server.js.map +1 -0
  59. package/dist/component/adapter.d.ts +130 -0
  60. package/dist/component/adapter.d.ts.map +1 -0
  61. package/dist/component/adapter.js +5 -0
  62. package/dist/component/adapter.js.map +1 -0
  63. package/dist/component/adapterTest.d.ts +10 -0
  64. package/dist/component/adapterTest.d.ts.map +1 -0
  65. package/dist/component/adapterTest.js +409 -0
  66. package/dist/component/adapterTest.js.map +1 -0
  67. package/dist/component/convex.config.d.ts +3 -0
  68. package/dist/component/convex.config.d.ts.map +1 -0
  69. package/dist/component/convex.config.js +4 -0
  70. package/dist/component/convex.config.js.map +1 -0
  71. package/dist/component/schema.d.ts +474 -0
  72. package/dist/component/schema.d.ts.map +1 -0
  73. package/dist/component/schema.js +139 -0
  74. package/dist/component/schema.js.map +1 -0
  75. package/dist/nextjs/client.d.ts +4 -0
  76. package/dist/nextjs/client.d.ts.map +1 -0
  77. package/dist/nextjs/client.js +37 -0
  78. package/dist/nextjs/client.js.map +1 -0
  79. package/dist/nextjs/index.d.ts +22 -0
  80. package/dist/nextjs/index.d.ts.map +1 -0
  81. package/dist/nextjs/index.js +98 -0
  82. package/dist/nextjs/index.js.map +1 -0
  83. package/dist/plugins/convex/client.d.ts +6 -0
  84. package/dist/plugins/convex/client.d.ts.map +1 -0
  85. package/dist/plugins/convex/client.js +7 -0
  86. package/dist/plugins/convex/client.js.map +1 -0
  87. package/dist/plugins/convex/index.d.ts +322 -0
  88. package/dist/plugins/convex/index.d.ts.map +1 -0
  89. package/dist/plugins/convex/index.js +422 -0
  90. package/dist/plugins/convex/index.js.map +1 -0
  91. package/dist/plugins/cross-domain/client.d.ts +132 -0
  92. package/dist/plugins/cross-domain/client.d.ts.map +1 -0
  93. package/dist/plugins/cross-domain/client.js +192 -0
  94. package/dist/plugins/cross-domain/client.js.map +1 -0
  95. package/dist/plugins/cross-domain/index.d.ts +51 -0
  96. package/dist/plugins/cross-domain/index.d.ts.map +1 -0
  97. package/dist/plugins/cross-domain/index.js +173 -0
  98. package/dist/plugins/cross-domain/index.js.map +1 -0
  99. package/dist/plugins/index.d.ts +3 -0
  100. package/dist/plugins/index.d.ts.map +1 -0
  101. package/dist/plugins/index.js +3 -0
  102. package/dist/plugins/index.js.map +1 -0
  103. package/dist/react/index.d.ts +80 -0
  104. package/dist/react/index.d.ts.map +1 -0
  105. package/dist/react/index.js +190 -0
  106. package/dist/react/index.js.map +1 -0
  107. package/dist/react-start/index.d.ts +13 -0
  108. package/dist/react-start/index.d.ts.map +1 -0
  109. package/dist/react-start/index.js +101 -0
  110. package/dist/react-start/index.js.map +1 -0
  111. package/dist/utils/index.d.ts +33 -0
  112. package/dist/utils/index.d.ts.map +1 -0
  113. package/dist/utils/index.js +91 -0
  114. package/dist/utils/index.js.map +1 -0
  115. package/package.json +208 -0
  116. package/src/auth-config.ts +80 -0
  117. package/src/auth-options.ts +54 -0
  118. package/src/auth.ts +4 -0
  119. package/src/client/adapter-utils.ts +639 -0
  120. package/src/client/adapter.test.ts +83 -0
  121. package/src/client/adapter.ts +363 -0
  122. package/src/client/create-api.ts +339 -0
  123. package/src/client/create-client.ts +452 -0
  124. package/src/client/create-schema.ts +166 -0
  125. package/src/client/index.ts +22 -0
  126. package/src/client/plugins/index.ts +2 -0
  127. package/src/component/_generated/api.ts +52 -0
  128. package/src/component/_generated/component.ts +2008 -0
  129. package/src/component/_generated/dataModel.ts +60 -0
  130. package/src/component/_generated/server.ts +161 -0
  131. package/src/component/adapter.ts +13 -0
  132. package/src/component/adapterTest.ts +505 -0
  133. package/src/component/convex.config.ts +5 -0
  134. package/src/component/schema.ts +142 -0
  135. package/src/nextjs/client.tsx +54 -0
  136. package/src/nextjs/index.ts +152 -0
  137. package/src/plugins/convex/client.ts +9 -0
  138. package/src/plugins/convex/index.ts +596 -0
  139. package/src/plugins/cross-domain/client.test.ts +217 -0
  140. package/src/plugins/cross-domain/client.ts +234 -0
  141. package/src/plugins/cross-domain/index.ts +199 -0
  142. package/src/plugins/index.ts +2 -0
  143. package/src/react/index.tsx +304 -0
  144. package/src/react-start/index.ts +153 -0
  145. package/src/react-start/vite-env.d.ts +2 -0
  146. package/src/test.ts +18 -0
  147. package/src/utils/index.ts +171 -0
@@ -0,0 +1,639 @@
1
+ import { asyncMap } from "convex-helpers";
2
+ import { v } from "convex/values";
3
+ import type { GenericId, Infer } from "convex/values";
4
+ import type {
5
+ DocumentByName,
6
+ GenericDataModel,
7
+ GenericQueryCtx,
8
+ PaginationOptions,
9
+ PaginationResult,
10
+ SchemaDefinition,
11
+ TableNamesInDataModel,
12
+ } from "convex/server";
13
+ import { stream } from "convex-helpers/server/stream";
14
+ import { mergedStream } from "convex-helpers/server/stream";
15
+ import { stripIndent } from "common-tags";
16
+ import type { BetterAuthDBSchema } from "better-auth/db";
17
+
18
+ export const adapterWhereValidator = v.object({
19
+ field: v.string(),
20
+ operator: v.optional(
21
+ v.union(
22
+ v.literal("lt"),
23
+ v.literal("lte"),
24
+ v.literal("gt"),
25
+ v.literal("gte"),
26
+ v.literal("eq"),
27
+ v.literal("in"),
28
+ v.literal("not_in"),
29
+ v.literal("ne"),
30
+ v.literal("contains"),
31
+ v.literal("starts_with"),
32
+ v.literal("ends_with")
33
+ )
34
+ ),
35
+ value: v.union(
36
+ v.string(),
37
+ v.number(),
38
+ v.boolean(),
39
+ v.array(v.string()),
40
+ v.array(v.number()),
41
+ v.null()
42
+ ),
43
+ connector: v.optional(v.union(v.literal("AND"), v.literal("OR"))),
44
+ });
45
+
46
+ export const adapterArgsValidator = v.object({
47
+ model: v.string(),
48
+ where: v.optional(v.array(adapterWhereValidator)),
49
+ sortBy: v.optional(
50
+ v.object({
51
+ field: v.string(),
52
+ direction: v.union(v.literal("asc"), v.literal("desc")),
53
+ })
54
+ ),
55
+ select: v.optional(v.array(v.string())),
56
+ limit: v.optional(v.number()),
57
+ offset: v.optional(v.number()),
58
+ });
59
+
60
+ const isUniqueField = (
61
+ betterAuthSchema: BetterAuthDBSchema,
62
+ model: string,
63
+ field: string
64
+ ) => {
65
+ const fields =
66
+ betterAuthSchema[model as keyof typeof betterAuthSchema]["fields"];
67
+ if (!fields) {
68
+ return false;
69
+ }
70
+ return Object.entries(fields)
71
+ .filter(([, value]) => value.unique)
72
+ .map(([key]) => key)
73
+ .includes(field);
74
+ };
75
+ export const hasUniqueFields = (
76
+ betterAuthSchema: BetterAuthDBSchema,
77
+ model: string,
78
+ input: Record<string, any>
79
+ ) => {
80
+ for (const field of Object.keys(input)) {
81
+ if (isUniqueField(betterAuthSchema, model, field)) {
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ };
87
+
88
+ const findIndex = (
89
+ schema: SchemaDefinition<any, any>,
90
+ args: {
91
+ model: string;
92
+ where?: {
93
+ field: string;
94
+ operator?:
95
+ | "lt"
96
+ | "lte"
97
+ | "gt"
98
+ | "gte"
99
+ | "eq"
100
+ | "in"
101
+ | "not_in"
102
+ | "ne"
103
+ | "contains"
104
+ | "starts_with"
105
+ | "ends_with";
106
+ value: string | number | boolean | null | string[] | number[];
107
+ connector?: "AND" | "OR";
108
+ }[];
109
+ sortBy?: {
110
+ field: string;
111
+ direction: "asc" | "desc";
112
+ };
113
+ }
114
+ ) => {
115
+ if (
116
+ (args.where?.length ?? 0) > 1 &&
117
+ args.where?.some((w) => w.connector === "OR")
118
+ ) {
119
+ throw new Error(
120
+ `OR connector not supported with multiple where statements in findIndex, split up the where statements before calling findIndex: ${JSON.stringify(args.where)}`
121
+ );
122
+ }
123
+ const where = args.where?.filter((w) => {
124
+ return (
125
+ (!w.operator ||
126
+ ["lt", "lte", "gt", "gte", "eq", "in", "not_in"].includes(
127
+ w.operator
128
+ )) &&
129
+ w.field !== "_id"
130
+ );
131
+ });
132
+ if (!where?.length && !args.sortBy) {
133
+ return;
134
+ }
135
+ const lowerBounds =
136
+ where?.filter((w) => w.operator === "lt" || w.operator === "lte") ?? [];
137
+ if (lowerBounds.length > 1) {
138
+ throw new Error(
139
+ `cannot have more than one lower bound where clause: ${JSON.stringify(where)}`
140
+ );
141
+ }
142
+ const upperBounds =
143
+ where?.filter((w) => w.operator === "gt" || w.operator === "gte") ?? [];
144
+ if (upperBounds.length > 1) {
145
+ throw new Error(
146
+ `cannot have more than one upper bound where clause: ${JSON.stringify(where)}`
147
+ );
148
+ }
149
+ const lowerBound = lowerBounds[0];
150
+ const upperBound = upperBounds[0];
151
+ if (lowerBound && upperBound && lowerBound.field !== upperBound.field) {
152
+ throw new Error(
153
+ `lower bound and upper bound must have the same field: ${JSON.stringify(where)}`
154
+ );
155
+ }
156
+ const boundField = lowerBound?.field || upperBound?.field;
157
+ if (
158
+ boundField &&
159
+ where?.some(
160
+ (w) => w.field === boundField && w !== lowerBound && w !== upperBound
161
+ )
162
+ ) {
163
+ throw new Error(
164
+ `too many where clauses on the bound field: ${JSON.stringify(where)}`
165
+ );
166
+ }
167
+ const indexEqFields =
168
+ where
169
+ ?.filter((w) => !w.operator || w.operator === "eq")
170
+ .sort((a, b) => {
171
+ return a.field.localeCompare(b.field);
172
+ })
173
+ .map((w) => [w.field, w.value]) ?? [];
174
+ if (!indexEqFields?.length && !boundField && !args.sortBy) {
175
+ return;
176
+ }
177
+ const table = schema.tables[args.model as keyof typeof schema.tables];
178
+ if (!table) {
179
+ throw new Error(`Table ${args.model} not found`);
180
+ }
181
+ const indexes = table[" indexes"]();
182
+ const sortField = args.sortBy?.field;
183
+
184
+ // We internally use _creationTime in place of Better Auth's createdAt
185
+ const indexFields = indexEqFields
186
+ .map(([field]) => field)
187
+ .concat(
188
+ boundField && boundField !== "createdAt"
189
+ ? `${indexEqFields.length ? "_" : ""}${boundField}`
190
+ : ""
191
+ )
192
+ .concat(
193
+ sortField && sortField !== "createdAt" && boundField !== sortField
194
+ ? `${indexEqFields.length || boundField ? "_" : ""}${sortField}`
195
+ : ""
196
+ )
197
+ .filter(Boolean);
198
+ if (!indexFields.length && !boundField && !sortField) {
199
+ return;
200
+ }
201
+ // Use the built in _creationTime index if bounding or sorting by createdAt
202
+ // with no other fields
203
+ const index = !indexFields.length
204
+ ? {
205
+ indexDescriptor: "by_creation_time",
206
+ fields: [],
207
+ }
208
+ : indexes.find(({ fields }: { fields: string[] }) => {
209
+ const fieldsMatch = indexFields.every(
210
+ (field, idx) => field === fields[idx]
211
+ );
212
+ // If sorting by createdAt, no intermediate fields can be on the index
213
+ // as they may override the createdAt sort order.
214
+ const boundFieldMatch =
215
+ boundField === "createdAt" || sortField === "createdAt"
216
+ ? indexFields.length === fields.length
217
+ : true;
218
+ return fieldsMatch && boundFieldMatch;
219
+ });
220
+ if (!index) {
221
+ return { indexFields };
222
+ }
223
+ return {
224
+ index: {
225
+ indexDescriptor: index.indexDescriptor,
226
+ fields: [...index.fields, "_creationTime"],
227
+ },
228
+ boundField,
229
+ sortField,
230
+ values: {
231
+ eq: indexEqFields.map(([, value]) => value),
232
+ lt: lowerBound?.operator === "lt" ? lowerBound.value : undefined,
233
+ lte: lowerBound?.operator === "lte" ? lowerBound.value : undefined,
234
+ gt: upperBound?.operator === "gt" ? upperBound.value : undefined,
235
+ gte: upperBound?.operator === "gte" ? upperBound.value : undefined,
236
+ },
237
+ };
238
+ };
239
+
240
+ export const checkUniqueFields = async <
241
+ Schema extends SchemaDefinition<any, any>,
242
+ >(
243
+ ctx: GenericQueryCtx<GenericDataModel>,
244
+ schema: Schema,
245
+ betterAuthSchema: BetterAuthDBSchema,
246
+ table: string,
247
+ input: Record<string, any>,
248
+ doc?: Record<string, any>
249
+ ) => {
250
+ if (!hasUniqueFields(betterAuthSchema, table, input)) {
251
+ return;
252
+ }
253
+ for (const field of Object.keys(input)) {
254
+ if (!isUniqueField(betterAuthSchema, table, field)) {
255
+ continue;
256
+ }
257
+ const { index } =
258
+ findIndex(schema, {
259
+ model: table,
260
+ where: [
261
+ { field, operator: "eq", value: input[field as keyof typeof input] },
262
+ ],
263
+ }) || {};
264
+ if (!index) {
265
+ throw new Error(`No index found for ${table}${field}`);
266
+ }
267
+ const existingDoc = await ctx.db
268
+ .query(table as any)
269
+ .withIndex(index.indexDescriptor, (q) =>
270
+ q.eq(field, input[field as keyof typeof input])
271
+ )
272
+ .unique();
273
+ if (existingDoc && existingDoc._id !== doc?._id) {
274
+ throw new Error(`${table} ${field} already exists`);
275
+ }
276
+ }
277
+ };
278
+
279
+ // This handles basic select (stripping out the other fields if there
280
+ // is a select arg).
281
+ export const selectFields = async <
282
+ T extends TableNamesInDataModel<GenericDataModel>,
283
+ D extends DocumentByName<GenericDataModel, T>,
284
+ >(
285
+ doc: D | null,
286
+ select?: string[]
287
+ ) => {
288
+ if (!doc) {
289
+ return null;
290
+ }
291
+ if (!select?.length) {
292
+ return doc;
293
+ }
294
+ return select.reduce((acc, field) => {
295
+ (acc as any)[field] = doc[field];
296
+ return acc;
297
+ }, {} as D);
298
+ };
299
+
300
+ // Manually filter an individual document by where clauses. This is used to
301
+ // simplify queries that can only return 0 or 1 documents, or "in" clauses that
302
+ // query multiple single documents in parallel.
303
+ const filterByWhere = <
304
+ T extends TableNamesInDataModel<GenericDataModel>,
305
+ D extends DocumentByName<GenericDataModel, T>,
306
+ >(
307
+ doc: D | null,
308
+ where?: Infer<typeof adapterWhereValidator>[],
309
+ // Optionally filter which where clauses to apply.
310
+ filterWhere?: (w: Infer<typeof adapterWhereValidator>) => any
311
+ ) => {
312
+ if (!doc) {
313
+ return false;
314
+ }
315
+ for (const w of where ?? []) {
316
+ if (filterWhere && !filterWhere(w)) {
317
+ continue;
318
+ }
319
+ const value = doc[w.field as keyof typeof doc] as Infer<
320
+ typeof adapterWhereValidator
321
+ >["value"];
322
+ const isLessThan = (val: typeof value, wVal: typeof w.value) => {
323
+ if (!wVal) {
324
+ return false;
325
+ }
326
+ if (!val) {
327
+ return true;
328
+ }
329
+ return val < wVal;
330
+ };
331
+ const isGreaterThan = (val: typeof value, wVal: typeof w.value) => {
332
+ if (!val) {
333
+ return false;
334
+ }
335
+ if (!wVal) {
336
+ return true;
337
+ }
338
+ return val > wVal;
339
+ };
340
+ const filter = (w: Infer<typeof adapterWhereValidator>) => {
341
+ switch (w.operator) {
342
+ case undefined:
343
+ case "eq": {
344
+ return value === w.value;
345
+ }
346
+ case "in": {
347
+ return Array.isArray(w.value) && (w.value as any[]).includes(value);
348
+ }
349
+ case "not_in": {
350
+ const result =
351
+ Array.isArray(w.value) && !(w.value as any[]).includes(value);
352
+ return result;
353
+ }
354
+ case "lt": {
355
+ return isLessThan(value, w.value);
356
+ }
357
+ case "lte": {
358
+ return value === w.value || isLessThan(value, w.value);
359
+ }
360
+ case "gt": {
361
+ return isGreaterThan(value, w.value);
362
+ }
363
+ case "gte": {
364
+ return value === w.value || isGreaterThan(value, w.value);
365
+ }
366
+ case "ne": {
367
+ return value !== w.value;
368
+ }
369
+ case "contains": {
370
+ return typeof value === "string" && value.includes(w.value as string);
371
+ }
372
+ case "starts_with": {
373
+ return (
374
+ typeof value === "string" && value.startsWith(w.value as string)
375
+ );
376
+ }
377
+ case "ends_with": {
378
+ return typeof value === "string" && value.endsWith(w.value as string);
379
+ }
380
+ }
381
+ };
382
+ if (!filter(w)) {
383
+ return false;
384
+ }
385
+ }
386
+ return true;
387
+ };
388
+
389
+ const generateQuery = (
390
+ ctx: GenericQueryCtx<GenericDataModel>,
391
+ schema: SchemaDefinition<any, any>,
392
+ args: Infer<typeof adapterArgsValidator>
393
+ ) => {
394
+ const { index, values, boundField, indexFields } =
395
+ findIndex(schema, args) ?? {};
396
+ const query = stream(ctx.db as any, schema).query(args.model as any);
397
+ const hasValues =
398
+ values?.eq?.length ||
399
+ values?.lt ||
400
+ values?.lte ||
401
+ values?.gt ||
402
+ values?.gte;
403
+ const indexedQuery =
404
+ index && index.indexDescriptor !== "by_creation_time"
405
+ ? query.withIndex(
406
+ index.indexDescriptor,
407
+ hasValues
408
+ ? (q: any) => {
409
+ for (const [idx, value] of (values?.eq ?? []).entries()) {
410
+ q = q.eq(index.fields[idx], value);
411
+ }
412
+ if (values?.lt) {
413
+ q = q.lt(boundField, values.lt);
414
+ }
415
+ if (values?.lte) {
416
+ q = q.lte(boundField, values.lte);
417
+ }
418
+ if (values?.gt) {
419
+ q = q.gt(boundField, values.gt);
420
+ }
421
+ if (values?.gte) {
422
+ q = q.gte(boundField, values.gte);
423
+ }
424
+ return q;
425
+ }
426
+ : undefined
427
+ )
428
+ : query;
429
+ const orderedQuery = args.sortBy
430
+ ? indexedQuery.order(args.sortBy.direction === "desc" ? "desc" : "asc")
431
+ : indexedQuery;
432
+ const filteredQuery = orderedQuery.filterWith(async (doc) => {
433
+ if (!index && indexFields?.length) {
434
+ // eslint-disable-next-line no-console
435
+ console.warn(
436
+ stripIndent`
437
+ Querying without an index on table "${args.model}".
438
+ This can cause performance issues, and may hit the document read limit.
439
+ To fix, add an index that begins with the following fields in order:
440
+ [${indexFields.join(", ")}]
441
+ `
442
+ );
443
+ // No index, handle all where clauses statically.
444
+ return filterByWhere(doc, args.where);
445
+ }
446
+ return filterByWhere(
447
+ doc,
448
+ args.where,
449
+ // Index used for all eq and range clauses, apply remaining clauses
450
+ // incompatible with Convex statically.
451
+ (w) =>
452
+ w.operator &&
453
+ ["contains", "starts_with", "ends_with", "ne", "not_in"].includes(
454
+ w.operator
455
+ )
456
+ );
457
+ });
458
+ return filteredQuery;
459
+ };
460
+
461
+ // This is the core function for reading from the database, it parses and
462
+ // validates where conditions, selects indexes, and allows the caller to
463
+ // optionally paginate as needed. Every response is a pagination result.
464
+ export const paginate = async <
465
+ Doc extends DocumentByName<GenericDataModel, T>,
466
+ T extends TableNamesInDataModel<GenericDataModel>,
467
+ >(
468
+ ctx: GenericQueryCtx<GenericDataModel>,
469
+ schema: SchemaDefinition<any, any>,
470
+ betterAuthSchema: BetterAuthDBSchema,
471
+ args: Infer<typeof adapterArgsValidator> & {
472
+ paginationOpts: PaginationOptions;
473
+ }
474
+ ): Promise<PaginationResult<Doc>> => {
475
+ if (args.offset) {
476
+ throw new Error(`offset not supported: ${JSON.stringify(args.offset)}`);
477
+ }
478
+ if (args.where?.some((w) => w.connector === "OR") && args.where?.length > 1) {
479
+ throw new Error(
480
+ `OR connector not supported with multiple where statements in paginate, split up the where statements before calling paginate: ${JSON.stringify(args.where)}`
481
+ );
482
+ }
483
+ if (
484
+ args.where?.some(
485
+ (w) =>
486
+ w.field === "_id" &&
487
+ w.operator &&
488
+ !["eq", "in", "not_in"].includes(w.operator)
489
+ )
490
+ ) {
491
+ throw new Error(
492
+ `_id can only be used with eq, in, or not_in operator: ${JSON.stringify(args.where)}`
493
+ );
494
+ }
495
+ // If any where clause is "eq" (or missing operator) on a unique field,
496
+ // we can only return a single document, so we get it and use any other
497
+ // where clauses as static filters.
498
+ const uniqueWhere = args.where?.find(
499
+ (w) =>
500
+ (!w.operator || w.operator === "eq") &&
501
+ (isUniqueField(betterAuthSchema, args.model, w.field) ||
502
+ w.field === "_id")
503
+ );
504
+ if (uniqueWhere) {
505
+ const { index } =
506
+ findIndex(schema, {
507
+ model: args.model,
508
+ where: [uniqueWhere],
509
+ }) || {};
510
+ const doc =
511
+ uniqueWhere.field === "_id"
512
+ ? await ctx.db.get(uniqueWhere.value as GenericId<T>)
513
+ : await ctx.db
514
+ .query(args.model as any)
515
+ .withIndex(index?.indexDescriptor as any, (q) =>
516
+ q.eq(index?.fields[0], uniqueWhere.value)
517
+ )
518
+ .unique();
519
+
520
+ // Apply all other clauses as static filters to our 0 or 1 result.
521
+ if (filterByWhere(doc, args.where, (w) => w !== uniqueWhere)) {
522
+ return {
523
+ page: [await selectFields(doc, args.select)].filter(Boolean) as Doc[],
524
+ isDone: true,
525
+ continueCursor: "",
526
+ };
527
+ }
528
+ return {
529
+ page: [],
530
+ isDone: true,
531
+ continueCursor: "",
532
+ };
533
+ }
534
+
535
+ const paginationOpts = {
536
+ ...args.paginationOpts,
537
+ // If maximumRowsRead is not at least 1 higher than numItems, bad cursors
538
+ // and incorrect paging will result (at least with convex-test).
539
+ maximumRowsRead: Math.max((args.paginationOpts.numItems ?? 0) + 1, 200),
540
+ };
541
+
542
+ // Large queries using "in" clause will crash, but these are only currently
543
+ // possible with the organization plugin listing all members with a high
544
+ // limit. For cases like this we need to create proper convex queries in
545
+ // the component as an alternative to using Better Auth api's.
546
+ const inWhere = args.where?.find((w) => w.operator === "in");
547
+ if (inWhere) {
548
+ if (!Array.isArray(inWhere.value)) {
549
+ throw new Error("in clause value must be an array");
550
+ }
551
+ // For ids, just use asyncMap + .get()
552
+ if (inWhere.field === "_id") {
553
+ const docs = await asyncMap(inWhere.value as any[], async (value) => {
554
+ return ctx.db.get(value as GenericId<T>);
555
+ });
556
+ const filteredDocs = docs
557
+ .flatMap((doc) => (doc ? [doc] : []))
558
+ .filter((doc) => filterByWhere(doc, args.where, (w) => w !== inWhere));
559
+
560
+ return {
561
+ page: filteredDocs.sort((a, b) => {
562
+ if (args.sortBy?.field === "createdAt") {
563
+ return args.sortBy.direction === "asc"
564
+ ? (a._creationTime as number) - (b._creationTime as number)
565
+ : (b._creationTime as number) - (a._creationTime as number);
566
+ }
567
+ if (args.sortBy) {
568
+ const aValue = a[args.sortBy.field as keyof typeof a];
569
+ const bValue = b[args.sortBy.field as keyof typeof b];
570
+ if (aValue === bValue) {
571
+ return 0;
572
+ }
573
+ return args.sortBy.direction === "asc"
574
+ ? aValue! > bValue!
575
+ ? 1
576
+ : -1
577
+ : aValue! > bValue!
578
+ ? -1
579
+ : 1;
580
+ }
581
+ return 0;
582
+ }) as Doc[],
583
+ isDone: true,
584
+ continueCursor: "",
585
+ };
586
+ }
587
+ const streams = inWhere.value.map((value) => {
588
+ return generateQuery(ctx, schema, {
589
+ ...args,
590
+ where: args.where?.map((w) => {
591
+ if (w === inWhere) {
592
+ return { ...w, operator: "eq", value };
593
+ }
594
+ return w;
595
+ }),
596
+ });
597
+ });
598
+ const result = await mergedStream(
599
+ streams,
600
+ [
601
+ args.sortBy?.field !== "createdAt" && args.sortBy?.field,
602
+ "_creationTime",
603
+ ].flatMap((f) => (f ? [f] : []))
604
+ ).paginate(paginationOpts);
605
+ return {
606
+ ...result,
607
+ page: await asyncMap(result.page, (doc) =>
608
+ selectFields(doc, args.select)
609
+ ),
610
+ };
611
+ }
612
+
613
+ const query = generateQuery(ctx, schema, args);
614
+ const result = await query.paginate(paginationOpts);
615
+ return {
616
+ ...result,
617
+ page: await asyncMap(result.page, (doc) => selectFields(doc, args.select)),
618
+ };
619
+ };
620
+
621
+ export const listOne = async <
622
+ Doc extends DocumentByName<GenericDataModel, T>,
623
+ T extends TableNamesInDataModel<GenericDataModel>,
624
+ >(
625
+ ctx: GenericQueryCtx<GenericDataModel>,
626
+ schema: SchemaDefinition<any, any>,
627
+ betterAuthSchema: BetterAuthDBSchema,
628
+ args: Infer<typeof adapterArgsValidator>
629
+ ): Promise<Doc | null> => {
630
+ return (
631
+ await paginate(ctx, schema, betterAuthSchema, {
632
+ ...args,
633
+ paginationOpts: {
634
+ numItems: 1,
635
+ cursor: null,
636
+ },
637
+ })
638
+ ).page[0] as Doc | null;
639
+ };