@convex-dev/better-auth 0.7.0-alpha.1 → 0.7.0-alpha.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 (116) hide show
  1. package/dist/commonjs/client/adapter.d.ts +10 -1
  2. package/dist/commonjs/client/adapter.d.ts.map +1 -1
  3. package/dist/commonjs/client/adapter.js +183 -192
  4. package/dist/commonjs/client/adapter.js.map +1 -1
  5. package/dist/commonjs/client/index.d.ts +238 -179
  6. package/dist/commonjs/client/index.d.ts.map +1 -1
  7. package/dist/commonjs/client/index.js +63 -67
  8. package/dist/commonjs/client/index.js.map +1 -1
  9. package/dist/commonjs/component/adapterTest.d.ts +19 -0
  10. package/dist/commonjs/component/adapterTest.d.ts.map +1 -0
  11. package/dist/commonjs/component/adapterTest.js +82 -0
  12. package/dist/commonjs/component/adapterTest.js.map +1 -0
  13. package/dist/commonjs/component/lib.d.ts +218 -548
  14. package/dist/commonjs/component/lib.d.ts.map +1 -1
  15. package/dist/commonjs/component/lib.js +315 -286
  16. package/dist/commonjs/component/lib.js.map +1 -1
  17. package/dist/commonjs/component/schema.d.ts +90 -28
  18. package/dist/commonjs/component/schema.d.ts.map +1 -1
  19. package/dist/commonjs/component/schema.js +76 -18
  20. package/dist/commonjs/component/schema.js.map +1 -1
  21. package/dist/commonjs/component/util.d.ts +148 -86
  22. package/dist/commonjs/component/util.d.ts.map +1 -1
  23. package/dist/commonjs/nextjs/index.d.ts.map +1 -1
  24. package/dist/commonjs/nextjs/index.js +12 -0
  25. package/dist/commonjs/nextjs/index.js.map +1 -1
  26. package/dist/commonjs/plugins/convex/index.d.ts +14 -11
  27. package/dist/commonjs/plugins/convex/index.d.ts.map +1 -1
  28. package/dist/commonjs/plugins/convex/index.js +10 -4
  29. package/dist/commonjs/plugins/convex/index.js.map +1 -1
  30. package/dist/commonjs/plugins/cross-domain/client.d.ts +1 -1
  31. package/dist/commonjs/plugins/cross-domain/index.d.ts +5 -3
  32. package/dist/commonjs/plugins/cross-domain/index.d.ts.map +1 -1
  33. package/dist/commonjs/plugins/cross-domain/index.js +19 -5
  34. package/dist/commonjs/plugins/cross-domain/index.js.map +1 -1
  35. package/dist/commonjs/react/client.d.ts +1 -1
  36. package/dist/commonjs/react/client.d.ts.map +1 -1
  37. package/dist/commonjs/react/client.js +3 -9
  38. package/dist/commonjs/react/client.js.map +1 -1
  39. package/dist/commonjs/react-start/index.d.ts +37 -3
  40. package/dist/commonjs/react-start/index.d.ts.map +1 -1
  41. package/dist/commonjs/react-start/index.js +20 -4
  42. package/dist/commonjs/react-start/index.js.map +1 -1
  43. package/dist/commonjs/utils/index.d.ts +2 -0
  44. package/dist/commonjs/utils/index.d.ts.map +1 -0
  45. package/dist/commonjs/utils/index.js +8 -0
  46. package/dist/commonjs/utils/index.js.map +1 -0
  47. package/dist/esm/client/adapter.d.ts +10 -1
  48. package/dist/esm/client/adapter.d.ts.map +1 -1
  49. package/dist/esm/client/adapter.js +183 -192
  50. package/dist/esm/client/adapter.js.map +1 -1
  51. package/dist/esm/client/index.d.ts +238 -179
  52. package/dist/esm/client/index.d.ts.map +1 -1
  53. package/dist/esm/client/index.js +63 -67
  54. package/dist/esm/client/index.js.map +1 -1
  55. package/dist/esm/component/adapterTest.d.ts +19 -0
  56. package/dist/esm/component/adapterTest.d.ts.map +1 -0
  57. package/dist/esm/component/adapterTest.js +82 -0
  58. package/dist/esm/component/adapterTest.js.map +1 -0
  59. package/dist/esm/component/lib.d.ts +218 -548
  60. package/dist/esm/component/lib.d.ts.map +1 -1
  61. package/dist/esm/component/lib.js +315 -286
  62. package/dist/esm/component/lib.js.map +1 -1
  63. package/dist/esm/component/schema.d.ts +90 -28
  64. package/dist/esm/component/schema.d.ts.map +1 -1
  65. package/dist/esm/component/schema.js +76 -18
  66. package/dist/esm/component/schema.js.map +1 -1
  67. package/dist/esm/component/util.d.ts +148 -86
  68. package/dist/esm/component/util.d.ts.map +1 -1
  69. package/dist/esm/nextjs/index.d.ts.map +1 -1
  70. package/dist/esm/nextjs/index.js +12 -0
  71. package/dist/esm/nextjs/index.js.map +1 -1
  72. package/dist/esm/plugins/convex/index.d.ts +14 -11
  73. package/dist/esm/plugins/convex/index.d.ts.map +1 -1
  74. package/dist/esm/plugins/convex/index.js +10 -4
  75. package/dist/esm/plugins/convex/index.js.map +1 -1
  76. package/dist/esm/plugins/cross-domain/client.d.ts +1 -1
  77. package/dist/esm/plugins/cross-domain/index.d.ts +5 -3
  78. package/dist/esm/plugins/cross-domain/index.d.ts.map +1 -1
  79. package/dist/esm/plugins/cross-domain/index.js +19 -5
  80. package/dist/esm/plugins/cross-domain/index.js.map +1 -1
  81. package/dist/esm/react/client.d.ts +1 -1
  82. package/dist/esm/react/client.d.ts.map +1 -1
  83. package/dist/esm/react/client.js +3 -9
  84. package/dist/esm/react/client.js.map +1 -1
  85. package/dist/esm/react-start/index.d.ts +37 -3
  86. package/dist/esm/react-start/index.d.ts.map +1 -1
  87. package/dist/esm/react-start/index.js +20 -4
  88. package/dist/esm/react-start/index.js.map +1 -1
  89. package/dist/esm/utils/index.d.ts +2 -0
  90. package/dist/esm/utils/index.d.ts.map +1 -0
  91. package/dist/esm/utils/index.js +8 -0
  92. package/dist/esm/utils/index.js.map +1 -0
  93. package/package.json +20 -5
  94. package/src/client/adapter.test.ts +144 -0
  95. package/src/client/adapter.ts +191 -195
  96. package/src/client/index.ts +64 -80
  97. package/src/component/_generated/api.d.ts +605 -149
  98. package/src/component/adapterTest.ts +141 -0
  99. package/src/component/lib.ts +444 -335
  100. package/src/component/schema.ts +81 -19
  101. package/src/nextjs/index.ts +17 -0
  102. package/src/plugins/convex/index.ts +12 -4
  103. package/src/plugins/cross-domain/index.ts +19 -5
  104. package/src/react/client.tsx +5 -11
  105. package/src/react-start/index.ts +33 -6
  106. package/src/react-start/vite-env.d.ts +2 -0
  107. package/dist/commonjs/react-router/index.d.ts +0 -10
  108. package/dist/commonjs/react-router/index.d.ts.map +0 -1
  109. package/dist/commonjs/react-router/index.js +0 -24
  110. package/dist/commonjs/react-router/index.js.map +0 -1
  111. package/dist/esm/react-router/index.d.ts +0 -10
  112. package/dist/esm/react-router/index.d.ts.map +0 -1
  113. package/dist/esm/react-router/index.js +0 -24
  114. package/dist/esm/react-router/index.js.map +0 -1
  115. package/src/client/cors.ts +0 -425
  116. /package/src/{util.ts → utils/index.ts} +0 -0
@@ -1,423 +1,532 @@
1
- import {
2
- action,
3
- mutation,
4
- query,
5
- QueryCtx,
6
- } from "../component/_generated/server";
1
+ import { mutation, query, QueryCtx } from "../component/_generated/server";
7
2
  import { asyncMap } from "convex-helpers";
8
- import { v } from "convex/values";
9
- import { api } from "../component/_generated/api";
3
+ import { Infer, v } from "convex/values";
10
4
  import { Doc, Id, TableNames } from "../component/_generated/dataModel";
11
- import schema from "../component/schema";
12
- import { paginationOptsValidator, PaginationResult } from "convex/server";
5
+ import schema, { specialFields } from "../component/schema";
6
+ import {
7
+ PaginationOptions,
8
+ paginationOptsValidator,
9
+ PaginationResult,
10
+ } from "convex/server";
13
11
  import { paginator } from "convex-helpers/server/pagination";
14
12
  import { partial } from "convex-helpers/validators";
15
13
 
16
- export const transformInput = (model: string, data: Record<string, any>) => {
17
- return {
18
- ...Object.fromEntries(
19
- Object.entries(data).map(([key, value]) => {
20
- if (value instanceof Date) {
21
- return [key, value.getTime()];
22
- }
23
- return [key, value];
24
- })
25
- ),
26
- };
27
- };
14
+ export const adapterWhereValidator = v.object({
15
+ field: v.string(),
16
+ operator: v.optional(
17
+ v.union(
18
+ v.literal("lt"),
19
+ v.literal("lte"),
20
+ v.literal("gt"),
21
+ v.literal("gte"),
22
+ v.literal("eq"),
23
+ v.literal("in"),
24
+ v.literal("ne"),
25
+ v.literal("contains"),
26
+ v.literal("starts_with"),
27
+ v.literal("ends_with")
28
+ )
29
+ ),
30
+ value: v.union(
31
+ v.string(),
32
+ v.number(),
33
+ v.boolean(),
34
+ v.array(v.string()),
35
+ v.array(v.number()),
36
+ v.null()
37
+ ),
38
+ connector: v.optional(v.union(v.literal("AND"), v.literal("OR"))),
39
+ });
28
40
 
29
- export const transformOutput = (
30
- { _id, _creationTime, ...data }: Doc<TableNames>,
31
- _model: string
32
- ) => {
33
- // Provide the expected id field, but it can be overwritten if
34
- // the model has an id field
35
- return { id: _id, ...data };
41
+ export const adapterArgsValidator = v.object({
42
+ model: v.string(),
43
+ where: v.optional(v.array(adapterWhereValidator)),
44
+ sortBy: v.optional(
45
+ v.object({
46
+ field: v.string(),
47
+ direction: v.union(v.literal("asc"), v.literal("desc")),
48
+ })
49
+ ),
50
+ select: v.optional(v.array(v.string())),
51
+ limit: v.optional(v.number()),
52
+ unique: v.optional(v.boolean()),
53
+ });
54
+
55
+ const getUniqueFields = (table: TableNames, input: Record<string, any>) => {
56
+ const fields = specialFields[table as keyof typeof specialFields];
57
+ if (!fields) {
58
+ return [];
59
+ }
60
+ return Object.entries(fields)
61
+ .filter(
62
+ ([key, value]) =>
63
+ value.unique && Object.keys(input).includes(key as keyof typeof input)
64
+ )
65
+ .map(([key]) => key);
36
66
  };
37
67
 
38
- // Get the session via sessionId in jwt claims
39
- export const getCurrentSession = query({
40
- args: {},
41
- handler: async (ctx) => {
42
- const identity = await ctx.auth.getUserIdentity();
43
- if (!identity) {
44
- return null;
68
+ const checkUniqueFields = async (
69
+ ctx: QueryCtx,
70
+ table: TableNames,
71
+ input: Record<string, any>,
72
+ doc?: Doc<any>
73
+ ) => {
74
+ const uniqueFields = getUniqueFields(table, input);
75
+ if (!uniqueFields.length) {
76
+ return;
77
+ }
78
+ for (const field of uniqueFields) {
79
+ const existingDoc = await ctx.db
80
+ .query(table as any)
81
+ .withIndex(field as any, (q) =>
82
+ q.eq(field, input[field as keyof typeof input])
83
+ )
84
+ .unique();
85
+ if (existingDoc && existingDoc._id !== doc?._id) {
86
+ throw new Error(`${table} ${field} already exists`);
45
87
  }
46
- return ctx.db.get(identity.sessionId as Id<"session">);
47
- },
48
- });
88
+ }
89
+ };
49
90
 
50
- export const getByHelper = async (
51
- ctx: QueryCtx,
52
- args: {
53
- table: string;
91
+ const findIndex = async (args: {
92
+ model: string;
93
+ where?: {
54
94
  field: string;
95
+ operator?: string;
55
96
  value: any;
56
- unique?: boolean;
97
+ connector?: "AND" | "OR";
98
+ }[];
99
+ sortBy?: {
100
+ field: string;
101
+ direction: "asc" | "desc";
102
+ };
103
+ }) => {
104
+ if (!args.where && !args.sortBy) {
105
+ return;
106
+ }
107
+ if (args.where?.some((w) => w.field === "id")) {
108
+ throw new Error("id is not a valid index field");
57
109
  }
110
+ if (args.where?.some((w) => w.connector && w.connector !== "AND")) {
111
+ throw new Error(
112
+ `OR connector not supported: ${JSON.stringify(args.where)}`
113
+ );
114
+ }
115
+ if (
116
+ args.where?.some(
117
+ (w) =>
118
+ w.operator &&
119
+ !["lt", "lte", "gt", "gte", "eq", "in"].includes(w.operator)
120
+ )
121
+ ) {
122
+ throw new Error(
123
+ `where clause not supported: ${JSON.stringify(args.where)}`
124
+ );
125
+ }
126
+ const lowerBounds =
127
+ args.where?.filter((w) => w.operator === "lt" || w.operator === "lte") ??
128
+ [];
129
+ if (lowerBounds.length > 1) {
130
+ throw new Error(
131
+ `cannot have more than one lower bound where clause: ${JSON.stringify(args.where)}`
132
+ );
133
+ }
134
+ const upperBounds =
135
+ args.where?.filter((w) => w.operator === "gt" || w.operator === "gte") ??
136
+ [];
137
+ if (upperBounds.length > 1) {
138
+ throw new Error(
139
+ `cannot have more than one upper bound where clause: ${JSON.stringify(args.where)}`
140
+ );
141
+ }
142
+ const lowerBound = lowerBounds[0];
143
+ const upperBound = upperBounds[0];
144
+ if (lowerBound && upperBound && lowerBound.field !== upperBound.field) {
145
+ throw new Error(
146
+ `lower bound and upper bound must have the same field: ${JSON.stringify(args.where)}`
147
+ );
148
+ }
149
+ const boundField = lowerBound?.field || upperBound?.field;
150
+ if (
151
+ boundField &&
152
+ args.where?.some(
153
+ (w) => w.field === boundField && w !== lowerBound && w !== upperBound
154
+ )
155
+ ) {
156
+ throw new Error(
157
+ `too many where clauses on the bound field: ${JSON.stringify(args.where)}`
158
+ );
159
+ }
160
+ const indexFields =
161
+ args.where
162
+ ?.filter((w) => !w.operator || w.operator === "eq")
163
+ .sort((a, b) => {
164
+ return a.field.localeCompare(b.field);
165
+ })
166
+ .map((w) => [w.field, w.value]) ?? [];
167
+ if (!indexFields?.length && !boundField && !args.sortBy) {
168
+ return;
169
+ }
170
+ const indexes =
171
+ schema.tables[args.model as keyof typeof schema.tables][" indexes"]();
172
+ const sortField = args.sortBy?.field;
173
+
174
+ // We internally use _creationTime in place of Better Auth's createdAt
175
+ const indexName = indexFields
176
+ .map(([field]) => field)
177
+ .join("_")
178
+ .concat(
179
+ boundField && boundField !== "createdAt"
180
+ ? `${indexFields.length ? "_" : ""}${boundField}`
181
+ : ""
182
+ )
183
+ .concat(
184
+ sortField && sortField !== "createdAt" && boundField !== sortField
185
+ ? `${indexFields.length || boundField ? "_" : ""}${sortField}`
186
+ : ""
187
+ );
188
+ if (!indexName && !boundField && !sortField) {
189
+ return;
190
+ }
191
+ // Use the built in creationTime index if bounding or sorting by createdAt
192
+ // with no other fields
193
+ const index = !indexName
194
+ ? {
195
+ indexDescriptor: "by_creation_time",
196
+ fields: [],
197
+ }
198
+ : indexes.find(({ indexDescriptor }) => {
199
+ return boundField === "createdAt" || sortField === "createdAt"
200
+ ? indexDescriptor === indexName
201
+ : indexDescriptor.startsWith(indexName);
202
+ });
203
+ if (!index) {
204
+ throw new Error(`Index ${indexName} not found for table ${args.model}`);
205
+ }
206
+ return {
207
+ index: {
208
+ indexDescriptor: index.indexDescriptor,
209
+ fields: [...index.fields, "_creationTime"],
210
+ },
211
+ boundField,
212
+ sortField,
213
+ values: {
214
+ eq: indexFields.map(([, value]) => value),
215
+ lt: lowerBound?.operator === "lt" ? lowerBound.value : undefined,
216
+ lte: lowerBound?.operator === "lte" ? lowerBound.value : undefined,
217
+ gt: upperBound?.operator === "gt" ? upperBound.value : undefined,
218
+ gte: upperBound?.operator === "gte" ? upperBound.value : undefined,
219
+ },
220
+ };
221
+ };
222
+
223
+ const selectFields = <T extends TableNames, D extends Doc<T>>(
224
+ doc: D | null,
225
+ select?: string[]
58
226
  ) => {
59
- if (args.field === "id") {
60
- return ctx.db.get(args.value);
227
+ if (!doc) {
228
+ return null;
229
+ }
230
+ if (!select?.length) {
231
+ return doc;
61
232
  }
62
- const query = ctx.db
63
- .query(args.table as any)
64
- .withIndex(args.field as any, (q) => q.eq(args.field, args.value));
65
- return args.unique ? await query.unique() : await query.first();
233
+ return select.reduce((acc, field) => {
234
+ (acc as any)[field] = doc[field];
235
+ return acc;
236
+ }, {} as D);
66
237
  };
67
238
 
68
- export const getByArgsValidator = {
69
- table: v.string(),
70
- field: v.string(),
71
- unique: v.optional(v.boolean()),
72
- value: v.union(
73
- v.string(),
74
- v.number(),
75
- v.boolean(),
76
- v.array(v.string()),
77
- v.array(v.number()),
78
- v.null()
79
- ),
239
+ // This is the core function for reading from the database, it parses and
240
+ // validates where conditions, selects indexes, and allows the caller to
241
+ // optionally paginate as needed.
242
+ const paginate = async (
243
+ ctx: QueryCtx,
244
+ args: Infer<typeof adapterArgsValidator> & {
245
+ paginationOpts: PaginationOptions;
246
+ }
247
+ ): Promise<PaginationResult<Doc<any>>> => {
248
+ // If any index is id, we can only return a single document
249
+ const idWhere = args.where?.find((w) => w.field === "id");
250
+ if (idWhere) {
251
+ const doc = await ctx.db.get(idWhere.value as Id<TableNames>);
252
+ return {
253
+ page: [selectFields(doc, args.select)].filter(Boolean),
254
+ isDone: true,
255
+ continueCursor: "",
256
+ };
257
+ }
258
+ const { index, values, boundField } = (await findIndex(args)) ?? {};
259
+ const query = paginator(ctx.db, schema).query(args.model as any);
260
+ const hasValues =
261
+ values?.eq?.length ||
262
+ values?.lt ||
263
+ values?.lte ||
264
+ values?.gt ||
265
+ values?.gte;
266
+ const indexedQuery =
267
+ index && index.indexDescriptor !== "by_creation_time"
268
+ ? query.withIndex(
269
+ index.indexDescriptor,
270
+ hasValues
271
+ ? (q: any) => {
272
+ for (const [idx, value] of (values?.eq ?? []).entries()) {
273
+ q = q.eq(index.fields[idx], value);
274
+ }
275
+ if (values?.lt) {
276
+ q = q.lt(boundField, values.lt);
277
+ }
278
+ if (values?.lte) {
279
+ q = q.lte(boundField, values.lte);
280
+ }
281
+ if (values?.gt) {
282
+ q = q.gt(boundField, values.gt);
283
+ }
284
+ if (values?.gte) {
285
+ q = q.gte(boundField, values.gte);
286
+ }
287
+ return q;
288
+ }
289
+ : undefined
290
+ )
291
+ : query;
292
+
293
+ const orderedQuery = args.sortBy
294
+ ? indexedQuery.order(args.sortBy.direction === "asc" ? "asc" : "desc")
295
+ : indexedQuery;
296
+ const result = await orderedQuery.paginate(args.paginationOpts);
297
+ return {
298
+ ...result,
299
+ page: result.page.map((doc) => selectFields(doc, args.select)),
300
+ };
80
301
  };
81
302
 
82
- // Generic functions
83
- export const getByQuery = query({
84
- args: getByArgsValidator,
85
- handler: async (ctx, args) => {
86
- const doc = await getByHelper(ctx, args);
87
- if (!doc) {
88
- return;
89
- }
90
- return transformOutput(doc, args.table);
91
- },
92
- });
93
- export { getByQuery as getBy };
303
+ const listOne = async (
304
+ ctx: QueryCtx,
305
+ args: Infer<typeof adapterArgsValidator>
306
+ ): Promise<Doc<any> | null> => {
307
+ return (
308
+ await paginate(ctx, {
309
+ ...args,
310
+ paginationOpts: {
311
+ numItems: 1,
312
+ cursor: null,
313
+ },
314
+ })
315
+ ).page[0];
316
+ };
94
317
 
95
318
  export const create = mutation({
96
- args: v.object({
319
+ args: {
97
320
  input: v.union(
98
- ...Object.values(schema.tables).map((table) =>
321
+ ...Object.entries(schema.tables).map(([model, table]) =>
99
322
  v.object({
100
- table: v.string(),
101
- ...table.validator.fields,
323
+ model: v.literal(model),
324
+ where: v.optional(v.array(adapterWhereValidator)),
325
+ data: v.object(table.validator.fields),
102
326
  })
103
327
  )
104
328
  ),
105
- }),
106
- handler: async (ctx, args) => {
107
- const { table, ...input } = args.input;
108
- const id = await ctx.db.insert(table as any, {
109
- ...input,
110
- });
111
- const doc = await ctx.db.get(id);
112
- if (!doc) {
113
- throw new Error(`Failed to create ${table}`);
114
- }
115
- return transformOutput(doc, table);
116
329
  },
117
- });
118
-
119
- export const updateArgsInputValidator = <T extends TableNames>(table: T) => {
120
- return v.object({
121
- table: v.literal(table),
122
- where: v.object({ field: v.string(), value: getByArgsValidator.value }),
123
- value: v.record(v.string(), v.any()),
124
- });
125
- };
126
-
127
- const updateArgsValidator = {
128
- input: v.union(
129
- updateArgsInputValidator("account"),
130
- updateArgsInputValidator("session"),
131
- updateArgsInputValidator("verification"),
132
- updateArgsInputValidator("user")
133
- ),
134
- };
135
-
136
- export const update = mutation({
137
- args: updateArgsValidator,
138
330
  handler: async (ctx, args) => {
139
- const { table, where, value } = args.input;
140
- const doc =
141
- where.field === "id"
142
- ? await ctx.db.get(where.value as Id<any>)
143
- : await getByHelper(ctx, { table, ...where });
144
- if (!doc) {
145
- throw new Error(`Failed to update ${table}`);
146
- }
147
- await ctx.db.patch(doc._id, value as any);
148
- const updatedDoc = await ctx.db.get(doc._id);
149
- if (!updatedDoc) {
150
- throw new Error(`Failed to update ${table}`);
151
- }
152
- return transformOutput(updatedDoc, table);
153
- },
154
- });
331
+ await checkUniqueFields(
332
+ ctx,
333
+ args.input.model as TableNames,
334
+ args.input.data
335
+ );
155
336
 
156
- export const deleteBy = mutation({
157
- args: getByArgsValidator,
158
- handler: async (ctx, args) => {
159
- const doc = await getByHelper(ctx, args);
337
+ const id = await ctx.db.insert(args.input.model as any, args.input.data);
338
+ const doc = await ctx.db.get(id);
160
339
  if (!doc) {
161
- return;
340
+ throw new Error(`Failed to create ${args.input.model}`);
162
341
  }
163
- await ctx.db.delete(doc._id);
164
- // onDeleteUser requires userId from the doc,
165
- // so just return the whole thing
166
342
  return doc;
167
343
  },
168
344
  });
169
345
 
170
- // Single purpose functions
171
- export const getAccountsByUserId = query({
172
- args: { userId: v.string(), limit: v.optional(v.number()) },
173
- handler: async (ctx, args) => {
174
- const query = ctx.db
175
- .query("account")
176
- .withIndex("userId", (q) => q.eq("userId", args.userId));
177
- const docs = args.limit
178
- ? await query.take(args.limit)
179
- : await query.collect();
180
- return docs.map((doc) => transformOutput(doc, "account"));
181
- },
182
- });
183
-
184
- export const getSessionsByUserId = query({
185
- args: { userId: v.string(), limit: v.optional(v.number()) },
346
+ export const findOne = query({
347
+ args: adapterArgsValidator,
186
348
  handler: async (ctx, args) => {
187
- const query = ctx.db
188
- .query("session")
189
- .withIndex("userId", (q) => q.eq("userId", args.userId));
190
- const docs = args.limit
191
- ? await query.take(args.limit)
192
- : await query.collect();
193
- return docs.map((doc) => transformOutput(doc, "session"));
349
+ return await listOne(ctx, args);
194
350
  },
195
351
  });
196
352
 
197
- export const getJwks = query({
353
+ export const findMany = query({
198
354
  args: {
199
- limit: v.optional(v.number()),
355
+ ...adapterArgsValidator.fields,
356
+ paginationOpts: paginationOptsValidator,
200
357
  },
201
358
  handler: async (ctx, args) => {
202
- const query = ctx.db.query("jwks");
203
- const docs = args.limit
204
- ? await query.take(args.limit)
205
- : await query.collect();
206
- return docs.map((doc) => transformOutput(doc, "jwks"));
359
+ return await paginate(ctx, args);
207
360
  },
208
361
  });
209
362
 
210
- export const listVerificationsByIdentifier = query({
363
+ export const updateOne = mutation({
211
364
  args: {
212
- identifier: v.string(),
213
- sortBy: v.optional(
214
- v.object({
215
- field: v.string(),
216
- direction: v.union(v.literal("asc"), v.literal("desc")),
217
- })
365
+ input: v.union(
366
+ ...Object.entries(schema.tables).map(([model, table]) =>
367
+ v.object({
368
+ model: v.literal(model),
369
+ where: v.optional(v.array(adapterWhereValidator)),
370
+ update: v.object(
371
+ Object.fromEntries(
372
+ Object.entries(table.validator.fields).map(([key, value]) => [
373
+ key,
374
+ value.isOptional === "required" ? v.optional(value) : value,
375
+ ])
376
+ )
377
+ ),
378
+ })
379
+ )
218
380
  ),
219
- limit: v.optional(v.number()),
220
381
  },
221
382
  handler: async (ctx, args) => {
222
- if (args.sortBy && args.sortBy.field !== "createdAt") {
223
- throw new Error(`Unsupported sortBy field: ${args.sortBy.field}`);
383
+ const doc = await listOne(ctx, args.input);
384
+ if (!doc) {
385
+ throw new Error(`Failed to update ${args.input.model}`);
224
386
  }
225
- const query = ctx.db
226
- .query("verification")
227
- .withIndex("identifier", (q) => q.eq("identifier", args.identifier))
228
- .order(
229
- args.sortBy?.field === "createdAt" && args.sortBy?.direction
230
- ? args.sortBy.direction
231
- : "asc"
232
- );
233
- const docs = args.limit
234
- ? await query.take(args.limit)
235
- : await query.collect();
236
- return docs.map((doc) => transformOutput(doc, "verification"));
237
- },
238
- });
239
-
240
- export const deleteOldVerificationsPage = mutation({
241
- args: {
242
- currentTimestamp: v.number(),
243
- paginationOpts: v.optional(paginationOptsValidator),
244
- },
245
- handler: async (ctx, args) => {
246
- const paginationOpts = args.paginationOpts ?? {
247
- numItems: 500,
248
- cursor: null,
249
- };
250
- const { page, ...result } = await paginator(ctx.db, schema)
251
- .query("verification")
252
- .withIndex("expiresAt", (q) => q.lt("expiresAt", args.currentTimestamp))
253
- .paginate(paginationOpts);
254
- await asyncMap(page, async (doc) => {
255
- await ctx.db.delete(doc._id);
256
- });
257
- return { ...result, count: page.length };
387
+ await checkUniqueFields(
388
+ ctx,
389
+ args.input.model as TableNames,
390
+ args.input.update,
391
+ doc
392
+ );
393
+ await ctx.db.patch(doc._id, args.input.update as any);
394
+ const updatedDoc = await ctx.db.get(doc._id);
395
+ if (!updatedDoc) {
396
+ throw new Error(`Failed to update ${args.input.model}`);
397
+ }
398
+ return updatedDoc;
258
399
  },
259
400
  });
260
401
 
261
- export const deleteOldVerifications = action({
402
+ export const updateMany = mutation({
262
403
  args: {
263
- currentTimestamp: v.number(),
404
+ ...adapterArgsValidator.fields,
405
+ update: v.optional(v.object(partial(schema.tables.user.validator.fields))),
406
+ paginationOpts: paginationOptsValidator,
264
407
  },
265
408
  handler: async (ctx, args) => {
266
- let count = 0;
267
- let cursor = null;
268
- let isDone = false;
269
- do {
270
- const result: Omit<PaginationResult<Doc<"verification">>, "page"> & {
271
- count: number;
272
- } = await ctx.runMutation(api.lib.deleteOldVerificationsPage, {
273
- currentTimestamp: args.currentTimestamp,
274
- paginationOpts: {
275
- numItems: 500,
276
- cursor,
277
- },
409
+ const { page, ...result } = await paginate(ctx, args);
410
+ if (args.update) {
411
+ const uniqueFields = getUniqueFields(
412
+ args.model as TableNames,
413
+ args.update ?? {}
414
+ );
415
+ if (uniqueFields.length && page.length > 1) {
416
+ throw new Error(
417
+ `Attempted to set unique fields in multiple documents in ${args.model} with the same value. Fields: ${uniqueFields.join(", ")}`
418
+ );
419
+ }
420
+ await asyncMap(page, async (doc) => {
421
+ await checkUniqueFields(
422
+ ctx,
423
+ args.model as TableNames,
424
+ args.update ?? {},
425
+ doc
426
+ );
427
+ await ctx.db.patch(doc._id, args.update as any);
278
428
  });
279
- count += result.count;
280
- cursor =
281
- result.pageStatus &&
282
- result.splitCursor &&
283
- ["SplitRecommended", "SplitRequired"].includes(result.pageStatus)
284
- ? result.splitCursor
285
- : result.continueCursor;
286
- isDone = result.isDone;
287
- } while (!isDone);
288
- return count;
429
+ }
430
+ return {
431
+ ...result,
432
+ count: page.length,
433
+ };
289
434
  },
290
435
  });
291
436
 
292
- export const deleteExpiredSessions = mutation({
293
- args: {
294
- userId: v.string(),
295
- expiresAt: v.number(),
296
- },
437
+ export const deleteOne = mutation({
438
+ args: adapterArgsValidator,
297
439
  handler: async (ctx, args) => {
298
- const docs = await ctx.db
299
- .query("session")
300
- .withIndex("userId_expiresAt", (q) =>
301
- q.eq("userId", args.userId).lt("expiresAt", args.expiresAt)
302
- )
303
- .collect();
304
- await asyncMap(docs, async (doc) => {
305
- await ctx.db.delete(doc._id);
306
- });
307
- return docs.length;
440
+ const doc = await listOne(ctx, args);
441
+ if (!doc) {
442
+ return;
443
+ }
444
+ await ctx.db.delete(doc._id);
445
+ return doc;
308
446
  },
309
447
  });
310
448
 
311
- export const deleteAllForUserPage = mutation({
449
+ export const deleteMany = mutation({
312
450
  args: {
313
- table: v.string(),
314
- userId: v.string(),
315
- paginationOpts: v.optional(paginationOptsValidator),
451
+ ...adapterArgsValidator.fields,
452
+ paginationOpts: paginationOptsValidator,
316
453
  },
317
454
  handler: async (ctx, args) => {
318
- const paginationOpts = args.paginationOpts ?? {
319
- numItems: 500,
320
- cursor: null,
321
- };
322
- const { page, ...result } = await paginator(ctx.db, schema)
323
- .query(args.table as any)
324
- .withIndex("userId", (q) => q.eq("userId", args.userId))
325
- .paginate(paginationOpts);
455
+ const { page, ...result } = await paginate(ctx, args);
326
456
  await asyncMap(page, async (doc) => {
327
457
  await ctx.db.delete(doc._id);
328
458
  });
329
- return { ...result, count: page.length };
330
- },
331
- });
332
-
333
- export const deleteAllForUser = action({
334
- args: {
335
- table: v.string(),
336
- userId: v.string(),
337
- },
338
- handler: async (ctx, args) => {
339
- let count = 0;
340
- let cursor = null;
341
- let isDone = false;
342
- do {
343
- const result: Omit<PaginationResult<Doc<"session">>, "page"> & {
344
- count: number;
345
- } = await ctx.runMutation(api.lib.deleteAllForUserPage, {
346
- table: args.table,
347
- userId: args.userId,
348
- paginationOpts: {
349
- numItems: 500,
350
- cursor,
351
- },
352
- });
353
- count += result.count;
354
- cursor =
355
- result.pageStatus &&
356
- result.splitCursor &&
357
- ["SplitRecommended", "SplitRequired"].includes(result.pageStatus)
358
- ? result.splitCursor
359
- : result.continueCursor;
360
- isDone = result.isDone;
361
- } while (!isDone);
362
- return count;
459
+ return {
460
+ ...result,
461
+ count: page.length,
462
+ };
363
463
  },
364
464
  });
365
465
 
366
- export const getAccountByAccountIdAndProviderId = query({
367
- args: { accountId: v.string(), providerId: v.string() },
368
- handler: async (ctx, args) => {
369
- const doc = await ctx.db
370
- .query("account")
371
- .withIndex("providerId_accountId", (q) =>
372
- q.eq("providerId", args.providerId).eq("accountId", args.accountId)
373
- )
374
- .unique();
375
- if (!doc) {
376
- return;
466
+ // Get the session via sessionId in jwt claims
467
+ // TODO: this needs a refresh, subquery only necessary for actions
468
+ export const getCurrentSession = query({
469
+ args: {},
470
+ handler: async (ctx) => {
471
+ const identity = await ctx.auth.getUserIdentity();
472
+ if (!identity) {
473
+ return null;
377
474
  }
378
- return transformOutput(doc, "account");
475
+ return ctx.db.get(identity.sessionId as Id<"session">);
379
476
  },
380
477
  });
381
478
 
382
- export const updateUserProviderAccounts = mutation({
383
- args: {
384
- userId: v.string(),
385
- providerId: v.string(),
386
- update: v.object(partial(schema.tables.account.validator.fields)),
387
- },
479
+ // TODO: rewrite functions below here to be dynamic
480
+
481
+ export const getIn = query({
482
+ args: adapterArgsValidator,
388
483
  handler: async (ctx, args) => {
389
- const docs = await ctx.db
390
- .query("account")
391
- .withIndex("userId_providerId", (q) =>
392
- q.eq("userId", args.userId).eq("providerId", args.providerId)
393
- )
394
- .collect();
395
- if (docs.length === 0) {
396
- return 0;
484
+ const where = args.where?.[0];
485
+ if (!where || where.operator !== "in" || args.where?.length !== 1) {
486
+ throw new Error("where must be a single in clause");
397
487
  }
398
- await asyncMap(docs, async (doc) => {
399
- await ctx.db.patch(doc._id, args.update);
400
- });
401
- return docs.length;
488
+ return (
489
+ await asyncMap(where.value as any[], async (value) => {
490
+ if (where.field === "id") {
491
+ return [await ctx.db.get(value as Id<TableNames>)];
492
+ }
493
+ const query = ctx.db
494
+ .query(args.model as any)
495
+ .withIndex(where.field as any, (q) => q.eq(where.field, value));
496
+ if (args.limit) {
497
+ return await query.take(args.limit);
498
+ }
499
+ return await query.collect();
500
+ })
501
+ )
502
+ .flat()
503
+ .filter(Boolean);
402
504
  },
403
505
  });
404
506
 
405
- export const updateTwoFactor = mutation({
507
+ export const deleteIn = mutation({
406
508
  args: {
407
- userId: v.string(),
408
- update: v.object(partial(schema.tables.twoFactor.validator.fields)),
509
+ input: v.union(
510
+ v.object({
511
+ table: v.literal("session"),
512
+ field: v.literal("token"),
513
+ values: v.array(v.string()),
514
+ })
515
+ ),
409
516
  },
410
517
  handler: async (ctx, args) => {
411
- const docs = await ctx.db
412
- .query("twoFactor")
413
- .withIndex("userId", (q) => q.eq("userId", args.userId))
414
- .collect();
415
- if (docs.length === 0) {
416
- return 0;
417
- }
418
- await asyncMap(docs, async (doc) => {
419
- await ctx.db.patch(doc._id, args.update);
518
+ const { table, field, values } = args.input;
519
+ const docs = await asyncMap(values, async (value) => {
520
+ const doc = await ctx.db
521
+ .query(table)
522
+ .withIndex(field as any, (q) => q.eq(field, value))
523
+ .unique();
524
+ if (!doc) {
525
+ return;
526
+ }
527
+ await ctx.db.delete(doc._id);
528
+ return doc;
420
529
  });
421
- return docs.length;
530
+ return docs.filter(Boolean).length;
422
531
  },
423
532
  });