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