@hammadj/better-auth-mongo-adapter 1.5.0-beta.9
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/.turbo/turbo-build.log +25 -0
- package/LICENSE.md +20 -0
- package/dist/index.d.mts +38 -0
- package/dist/index.mjs +399 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +1 -0
- package/src/mongodb-adapter.test.ts +12 -0
- package/src/mongodb-adapter.ts +701 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +8 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
> @hammadj/better-auth-mongo-adapter@1.5.0-beta.9 build /Users/hammadjutt/Dev/better-auth-fork/packages/mongo-adapter
|
|
3
|
+
> tsdown
|
|
4
|
+
|
|
5
|
+
[34mβΉ[39m tsdown [2mv0.20.1[22m powered by rolldown [2mv1.0.0-rc.1[22m
|
|
6
|
+
[34mβΉ[39m config file: [4m/Users/hammadjutt/Dev/better-auth-fork/packages/mongo-adapter/tsdown.config.ts[24m
|
|
7
|
+
[34mβΉ[39m entry: [34msrc/index.ts[39m
|
|
8
|
+
[34mβΉ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
+
[34mβΉ[39m Build start
|
|
10
|
+
[34mβΉ[39m Cleaning 2 files
|
|
11
|
+
[34mβΉ[39m [2mdist/[22m[1mindex.mjs[22m [2m12.71 kB[22m [2mβ gzip: 3.09 kB[22m
|
|
12
|
+
[34mβΉ[39m [2mdist/[22mindex.mjs.map [2m30.74 kB[22m [2mβ gzip: 6.70 kB[22m
|
|
13
|
+
[34mβΉ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.23 kB[22m [2mβ gzip: 0.57 kB[22m
|
|
14
|
+
[34mβΉ[39m 3 files, total: 44.69 kB
|
|
15
|
+
src/mongodb-adapter.ts (9:37) [33m[UNRESOLVED_IMPORT] Warning:[0m Could not resolve '@better-auth/core/db/adapter' in src/mongodb-adapter.ts
|
|
16
|
+
[38;5;246mβ[0m[38;5;246mβ[0m[38;5;246m[[0m src/mongodb-adapter.ts:9:38 [38;5;246m][0m
|
|
17
|
+
[38;5;246mβ[0m
|
|
18
|
+
[38;5;246m9 β[0m [38;5;249mi[0m[38;5;249mm[0m[38;5;249mp[0m[38;5;249mo[0m[38;5;249mr[0m[38;5;249mt[0m[38;5;249m [0m[38;5;249m{[0m[38;5;249m [0m[38;5;249mc[0m[38;5;249mr[0m[38;5;249me[0m[38;5;249ma[0m[38;5;249mt[0m[38;5;249me[0m[38;5;249mA[0m[38;5;249md[0m[38;5;249ma[0m[38;5;249mp[0m[38;5;249mt[0m[38;5;249me[0m[38;5;249mr[0m[38;5;249mF[0m[38;5;249ma[0m[38;5;249mc[0m[38;5;249mt[0m[38;5;249mo[0m[38;5;249mr[0m[38;5;249my[0m[38;5;249m [0m[38;5;249m}[0m[38;5;249m [0m[38;5;249mf[0m[38;5;249mr[0m[38;5;249mo[0m[38;5;249mm[0m[38;5;249m [0m"@better-auth/core/db/adapter"[38;5;249m;[0m
|
|
19
|
+
[38;5;240m β[0m ββββββββββββββββ¬ββββββββββββββ
|
|
20
|
+
[38;5;240m β[0m β°ββββββββββββββββ Module not found, treating it as an external dependency
|
|
21
|
+
[38;5;246mββββ―[0m
|
|
22
|
+
|
|
23
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
24
|
+
|
|
25
|
+
[32mβ[39m Build complete in [32m4921ms[39m
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2024 - present, Bereket Engida
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the βSoftwareβ), to deal in
|
|
6
|
+
the Software without restriction, including without limitation the rights to
|
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
8
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
16
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
19
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
20
|
+
DEALINGS IN THE SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DBAdapter, DBAdapterDebugLogOption } from "@better-auth/core/db/adapter";
|
|
2
|
+
import { Db, MongoClient } from "mongodb";
|
|
3
|
+
import { BetterAuthOptions } from "@better-auth/core";
|
|
4
|
+
|
|
5
|
+
//#region src/mongodb-adapter.d.ts
|
|
6
|
+
interface MongoDBAdapterConfig {
|
|
7
|
+
/**
|
|
8
|
+
* MongoDB client instance
|
|
9
|
+
* If not provided, Database transactions won't be enabled.
|
|
10
|
+
*/
|
|
11
|
+
client?: MongoClient | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Enable debug logs for the adapter
|
|
14
|
+
*
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
debugLogs?: DBAdapterDebugLogOption | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Use plural table names
|
|
20
|
+
*
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
usePlural?: boolean | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to execute multiple operations in a transaction.
|
|
26
|
+
*
|
|
27
|
+
* β οΈ Important:
|
|
28
|
+
* - Defaults to `true` when a MongoDB client is provided.
|
|
29
|
+
* - If your MongoDB instance does not support transactions
|
|
30
|
+
* (e.g. standalone server without a replica set),
|
|
31
|
+
* you must explicitly set `transaction: false`.
|
|
32
|
+
*/
|
|
33
|
+
transaction?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
declare const mongodbAdapter: (db: Db, config?: MongoDBAdapterConfig | undefined) => (options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { MongoDBAdapterConfig, mongodbAdapter };
|
|
38
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { createAdapterFactory } from "@better-auth/core/db/adapter";
|
|
2
|
+
import { ObjectId } from "mongodb";
|
|
3
|
+
|
|
4
|
+
//#region src/mongodb-adapter.ts
|
|
5
|
+
var MongoAdapterError = class extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "MongoAdapterError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const mongodbAdapter = (db, config) => {
|
|
13
|
+
let lazyOptions;
|
|
14
|
+
const getCustomIdGenerator = (options) => {
|
|
15
|
+
const generator = options.advanced?.database?.generateId;
|
|
16
|
+
if (typeof generator === "function") return generator;
|
|
17
|
+
};
|
|
18
|
+
const createCustomAdapter = (db, session) => ({ getFieldAttributes, getFieldName, schema, getDefaultModelName, options }) => {
|
|
19
|
+
const customIdGen = getCustomIdGenerator(options);
|
|
20
|
+
function serializeID({ field, value, model }) {
|
|
21
|
+
if (customIdGen) return value;
|
|
22
|
+
model = getDefaultModelName(model);
|
|
23
|
+
if (field === "id" || field === "_id" || schema[model].fields[field]?.references?.field === "id") {
|
|
24
|
+
if (value === null || value === void 0) return value;
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
if (value instanceof ObjectId) return value;
|
|
27
|
+
if (Array.isArray(value)) return value.map((v) => {
|
|
28
|
+
if (v === null || v === void 0) return v;
|
|
29
|
+
if (typeof v === "string") try {
|
|
30
|
+
return new ObjectId(v);
|
|
31
|
+
} catch {
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
if (v instanceof ObjectId) return v;
|
|
35
|
+
throw new MongoAdapterError("INVALID_ID", "Invalid id value");
|
|
36
|
+
});
|
|
37
|
+
throw new MongoAdapterError("INVALID_ID", "Invalid id value");
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return new ObjectId(value);
|
|
41
|
+
} catch {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function convertWhereClause({ where, model }) {
|
|
48
|
+
if (!where.length) return {};
|
|
49
|
+
const conditions = where.map((w) => {
|
|
50
|
+
const { field: field_, value, operator = "eq", connector = "AND" } = w;
|
|
51
|
+
let condition;
|
|
52
|
+
let field = getFieldName({
|
|
53
|
+
model,
|
|
54
|
+
field: field_
|
|
55
|
+
});
|
|
56
|
+
if (field === "id") field = "_id";
|
|
57
|
+
switch (operator.toLowerCase()) {
|
|
58
|
+
case "eq":
|
|
59
|
+
condition = { [field]: serializeID({
|
|
60
|
+
field,
|
|
61
|
+
value,
|
|
62
|
+
model
|
|
63
|
+
}) };
|
|
64
|
+
break;
|
|
65
|
+
case "in":
|
|
66
|
+
condition = { [field]: { $in: Array.isArray(value) ? value.map((v) => serializeID({
|
|
67
|
+
field,
|
|
68
|
+
value: v,
|
|
69
|
+
model
|
|
70
|
+
})) : [serializeID({
|
|
71
|
+
field,
|
|
72
|
+
value,
|
|
73
|
+
model
|
|
74
|
+
})] } };
|
|
75
|
+
break;
|
|
76
|
+
case "not_in":
|
|
77
|
+
condition = { [field]: { $nin: Array.isArray(value) ? value.map((v) => serializeID({
|
|
78
|
+
field,
|
|
79
|
+
value: v,
|
|
80
|
+
model
|
|
81
|
+
})) : [serializeID({
|
|
82
|
+
field,
|
|
83
|
+
value,
|
|
84
|
+
model
|
|
85
|
+
})] } };
|
|
86
|
+
break;
|
|
87
|
+
case "gt":
|
|
88
|
+
condition = { [field]: { $gt: serializeID({
|
|
89
|
+
field,
|
|
90
|
+
value,
|
|
91
|
+
model
|
|
92
|
+
}) } };
|
|
93
|
+
break;
|
|
94
|
+
case "gte":
|
|
95
|
+
condition = { [field]: { $gte: serializeID({
|
|
96
|
+
field,
|
|
97
|
+
value,
|
|
98
|
+
model
|
|
99
|
+
}) } };
|
|
100
|
+
break;
|
|
101
|
+
case "lt":
|
|
102
|
+
condition = { [field]: { $lt: serializeID({
|
|
103
|
+
field,
|
|
104
|
+
value,
|
|
105
|
+
model
|
|
106
|
+
}) } };
|
|
107
|
+
break;
|
|
108
|
+
case "lte":
|
|
109
|
+
condition = { [field]: { $lte: serializeID({
|
|
110
|
+
field,
|
|
111
|
+
value,
|
|
112
|
+
model
|
|
113
|
+
}) } };
|
|
114
|
+
break;
|
|
115
|
+
case "ne":
|
|
116
|
+
condition = { [field]: { $ne: serializeID({
|
|
117
|
+
field,
|
|
118
|
+
value,
|
|
119
|
+
model
|
|
120
|
+
}) } };
|
|
121
|
+
break;
|
|
122
|
+
case "contains":
|
|
123
|
+
condition = { [field]: { $regex: `.*${escapeForMongoRegex(value)}.*` } };
|
|
124
|
+
break;
|
|
125
|
+
case "starts_with":
|
|
126
|
+
condition = { [field]: { $regex: `^${escapeForMongoRegex(value)}` } };
|
|
127
|
+
break;
|
|
128
|
+
case "ends_with":
|
|
129
|
+
condition = { [field]: { $regex: `${escapeForMongoRegex(value)}$` } };
|
|
130
|
+
break;
|
|
131
|
+
default: throw new MongoAdapterError("UNSUPPORTED_OPERATOR", `Unsupported operator: ${operator}`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
condition,
|
|
135
|
+
connector
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
if (conditions.length === 1) return conditions[0].condition;
|
|
139
|
+
const andConditions = conditions.filter((c) => c.connector === "AND").map((c) => c.condition);
|
|
140
|
+
const orConditions = conditions.filter((c) => c.connector === "OR").map((c) => c.condition);
|
|
141
|
+
let clause = {};
|
|
142
|
+
if (andConditions.length) clause = {
|
|
143
|
+
...clause,
|
|
144
|
+
$and: andConditions
|
|
145
|
+
};
|
|
146
|
+
if (orConditions.length) clause = {
|
|
147
|
+
...clause,
|
|
148
|
+
$or: orConditions
|
|
149
|
+
};
|
|
150
|
+
return clause;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
async create({ model, data: values }) {
|
|
154
|
+
return {
|
|
155
|
+
_id: (await db.collection(model).insertOne(values, { session })).insertedId.toString(),
|
|
156
|
+
...values
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
async findOne({ model, where, select, join }) {
|
|
160
|
+
const pipeline = [where ? { $match: convertWhereClause({
|
|
161
|
+
where,
|
|
162
|
+
model
|
|
163
|
+
}) } : { $match: {} }];
|
|
164
|
+
if (join) for (const [joinedModel, joinConfig] of Object.entries(join)) {
|
|
165
|
+
const localField = getFieldName({
|
|
166
|
+
field: joinConfig.on.from,
|
|
167
|
+
model
|
|
168
|
+
});
|
|
169
|
+
const foreignField = getFieldName({
|
|
170
|
+
field: joinConfig.on.to,
|
|
171
|
+
model: joinedModel
|
|
172
|
+
});
|
|
173
|
+
const localFieldName = localField === "id" ? "_id" : localField;
|
|
174
|
+
const foreignFieldName = foreignField === "id" ? "_id" : foreignField;
|
|
175
|
+
const isUnique = (schema[getDefaultModelName(joinedModel)]?.fields[joinConfig.on.to])?.unique === true;
|
|
176
|
+
const shouldLimit = !isUnique && joinConfig.limit !== void 0;
|
|
177
|
+
const limit = joinConfig.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
|
|
178
|
+
if (shouldLimit && limit > 0) {
|
|
179
|
+
const foreignFieldRef = `$${foreignFieldName}`;
|
|
180
|
+
pipeline.push({ $lookup: {
|
|
181
|
+
from: joinedModel,
|
|
182
|
+
let: { localFieldValue: `$${localFieldName}` },
|
|
183
|
+
pipeline: [{ $match: { $expr: { $eq: [foreignFieldRef, "$$localFieldValue"] } } }, { $limit: limit }],
|
|
184
|
+
as: joinedModel
|
|
185
|
+
} });
|
|
186
|
+
} else pipeline.push({ $lookup: {
|
|
187
|
+
from: joinedModel,
|
|
188
|
+
localField: localFieldName,
|
|
189
|
+
foreignField: foreignFieldName,
|
|
190
|
+
as: joinedModel
|
|
191
|
+
} });
|
|
192
|
+
if (isUnique) pipeline.push({ $unwind: {
|
|
193
|
+
path: `$${joinedModel}`,
|
|
194
|
+
preserveNullAndEmptyArrays: true
|
|
195
|
+
} });
|
|
196
|
+
}
|
|
197
|
+
if (select) {
|
|
198
|
+
const projection = {};
|
|
199
|
+
select.forEach((field) => {
|
|
200
|
+
projection[getFieldName({
|
|
201
|
+
field,
|
|
202
|
+
model
|
|
203
|
+
})] = 1;
|
|
204
|
+
});
|
|
205
|
+
if (join) for (const joinedModel of Object.keys(join)) projection[joinedModel] = 1;
|
|
206
|
+
pipeline.push({ $project: projection });
|
|
207
|
+
}
|
|
208
|
+
pipeline.push({ $limit: 1 });
|
|
209
|
+
const res = await db.collection(model).aggregate(pipeline, { session }).toArray();
|
|
210
|
+
if (!res || res.length === 0) return null;
|
|
211
|
+
return res[0];
|
|
212
|
+
},
|
|
213
|
+
async findMany({ model, where, limit, offset, sortBy, join }) {
|
|
214
|
+
const pipeline = [where ? { $match: convertWhereClause({
|
|
215
|
+
where,
|
|
216
|
+
model
|
|
217
|
+
}) } : { $match: {} }];
|
|
218
|
+
if (join) for (const [joinedModel, joinConfig] of Object.entries(join)) {
|
|
219
|
+
const localField = getFieldName({
|
|
220
|
+
field: joinConfig.on.from,
|
|
221
|
+
model
|
|
222
|
+
});
|
|
223
|
+
const foreignField = getFieldName({
|
|
224
|
+
field: joinConfig.on.to,
|
|
225
|
+
model: joinedModel
|
|
226
|
+
});
|
|
227
|
+
const localFieldName = localField === "id" ? "_id" : localField;
|
|
228
|
+
const foreignFieldName = foreignField === "id" ? "_id" : foreignField;
|
|
229
|
+
const isUnique = getFieldAttributes({
|
|
230
|
+
model: joinedModel,
|
|
231
|
+
field: joinConfig.on.to
|
|
232
|
+
})?.unique === true;
|
|
233
|
+
const shouldLimit = joinConfig.relation !== "one-to-one" && joinConfig.limit !== void 0;
|
|
234
|
+
const limit = joinConfig.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
|
|
235
|
+
if (shouldLimit && limit > 0) {
|
|
236
|
+
const foreignFieldRef = `$${foreignFieldName}`;
|
|
237
|
+
pipeline.push({ $lookup: {
|
|
238
|
+
from: joinedModel,
|
|
239
|
+
let: { localFieldValue: `$${localFieldName}` },
|
|
240
|
+
pipeline: [{ $match: { $expr: { $eq: [foreignFieldRef, "$$localFieldValue"] } } }, { $limit: limit }],
|
|
241
|
+
as: joinedModel
|
|
242
|
+
} });
|
|
243
|
+
} else pipeline.push({ $lookup: {
|
|
244
|
+
from: joinedModel,
|
|
245
|
+
localField: localFieldName,
|
|
246
|
+
foreignField: foreignFieldName,
|
|
247
|
+
as: joinedModel
|
|
248
|
+
} });
|
|
249
|
+
if (isUnique) pipeline.push({ $unwind: {
|
|
250
|
+
path: `$${joinedModel}`,
|
|
251
|
+
preserveNullAndEmptyArrays: true
|
|
252
|
+
} });
|
|
253
|
+
}
|
|
254
|
+
if (sortBy) pipeline.push({ $sort: { [getFieldName({
|
|
255
|
+
field: sortBy.field,
|
|
256
|
+
model
|
|
257
|
+
})]: sortBy.direction === "desc" ? -1 : 1 } });
|
|
258
|
+
if (offset) pipeline.push({ $skip: offset });
|
|
259
|
+
if (limit) pipeline.push({ $limit: limit });
|
|
260
|
+
return await db.collection(model).aggregate(pipeline, { session }).toArray();
|
|
261
|
+
},
|
|
262
|
+
async count({ model, where }) {
|
|
263
|
+
const pipeline = [where ? { $match: convertWhereClause({
|
|
264
|
+
where,
|
|
265
|
+
model
|
|
266
|
+
}) } : { $match: {} }, { $count: "total" }];
|
|
267
|
+
const res = await db.collection(model).aggregate(pipeline, { session }).toArray();
|
|
268
|
+
if (!res || res.length === 0) return 0;
|
|
269
|
+
return res[0]?.total ?? 0;
|
|
270
|
+
},
|
|
271
|
+
async update({ model, where, update: values }) {
|
|
272
|
+
const clause = convertWhereClause({
|
|
273
|
+
where,
|
|
274
|
+
model
|
|
275
|
+
});
|
|
276
|
+
const doc = (await db.collection(model).findOneAndUpdate(clause, { $set: values }, {
|
|
277
|
+
session,
|
|
278
|
+
returnDocument: "after",
|
|
279
|
+
includeResultMetadata: true
|
|
280
|
+
}))?.value ?? null;
|
|
281
|
+
if (!doc) return null;
|
|
282
|
+
return doc;
|
|
283
|
+
},
|
|
284
|
+
async updateMany({ model, where, update: values }) {
|
|
285
|
+
const clause = convertWhereClause({
|
|
286
|
+
where,
|
|
287
|
+
model
|
|
288
|
+
});
|
|
289
|
+
return (await db.collection(model).updateMany(clause, { $set: values }, { session })).modifiedCount;
|
|
290
|
+
},
|
|
291
|
+
async delete({ model, where }) {
|
|
292
|
+
const clause = convertWhereClause({
|
|
293
|
+
where,
|
|
294
|
+
model
|
|
295
|
+
});
|
|
296
|
+
await db.collection(model).deleteOne(clause, { session });
|
|
297
|
+
},
|
|
298
|
+
async deleteMany({ model, where }) {
|
|
299
|
+
const clause = convertWhereClause({
|
|
300
|
+
where,
|
|
301
|
+
model
|
|
302
|
+
});
|
|
303
|
+
return (await db.collection(model).deleteMany(clause, { session })).deletedCount;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
let lazyAdapter = null;
|
|
308
|
+
let adapterOptions = null;
|
|
309
|
+
adapterOptions = {
|
|
310
|
+
config: {
|
|
311
|
+
adapterId: "mongodb-adapter",
|
|
312
|
+
adapterName: "MongoDB Adapter",
|
|
313
|
+
usePlural: config?.usePlural ?? false,
|
|
314
|
+
debugLogs: config?.debugLogs ?? false,
|
|
315
|
+
mapKeysTransformInput: { id: "_id" },
|
|
316
|
+
mapKeysTransformOutput: { _id: "id" },
|
|
317
|
+
supportsArrays: true,
|
|
318
|
+
supportsNumericIds: false,
|
|
319
|
+
transaction: config?.client && (config?.transaction ?? true) ? async (cb) => {
|
|
320
|
+
if (!config.client) return cb(lazyAdapter(lazyOptions));
|
|
321
|
+
const session = config.client.startSession();
|
|
322
|
+
try {
|
|
323
|
+
session.startTransaction();
|
|
324
|
+
const result = await cb(createAdapterFactory({
|
|
325
|
+
config: adapterOptions.config,
|
|
326
|
+
adapter: createCustomAdapter(db, session)
|
|
327
|
+
})(lazyOptions));
|
|
328
|
+
await session.commitTransaction();
|
|
329
|
+
return result;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
await session.abortTransaction();
|
|
332
|
+
throw err;
|
|
333
|
+
} finally {
|
|
334
|
+
await session.endSession();
|
|
335
|
+
}
|
|
336
|
+
} : false,
|
|
337
|
+
customTransformInput({ action, data, field, fieldAttributes, schema, model, options }) {
|
|
338
|
+
const customIdGen = getCustomIdGenerator(options);
|
|
339
|
+
if (field === "_id" || fieldAttributes.references?.field === "id") {
|
|
340
|
+
if (customIdGen) return data;
|
|
341
|
+
if (action !== "create") return data;
|
|
342
|
+
if (Array.isArray(data)) return data.map((v) => {
|
|
343
|
+
if (typeof v === "string") try {
|
|
344
|
+
return new ObjectId(v);
|
|
345
|
+
} catch {
|
|
346
|
+
return v;
|
|
347
|
+
}
|
|
348
|
+
return v;
|
|
349
|
+
});
|
|
350
|
+
if (typeof data === "string") try {
|
|
351
|
+
return new ObjectId(data);
|
|
352
|
+
} catch {
|
|
353
|
+
return data;
|
|
354
|
+
}
|
|
355
|
+
if (fieldAttributes?.references?.field === "id" && !fieldAttributes?.required && data === null) return null;
|
|
356
|
+
return new ObjectId();
|
|
357
|
+
}
|
|
358
|
+
return data;
|
|
359
|
+
},
|
|
360
|
+
customTransformOutput({ data, field, fieldAttributes }) {
|
|
361
|
+
if (field === "id" || fieldAttributes.references?.field === "id") {
|
|
362
|
+
if (data instanceof ObjectId) return data.toHexString();
|
|
363
|
+
if (Array.isArray(data)) return data.map((v) => {
|
|
364
|
+
if (v instanceof ObjectId) return v.toHexString();
|
|
365
|
+
return v;
|
|
366
|
+
});
|
|
367
|
+
return data;
|
|
368
|
+
}
|
|
369
|
+
return data;
|
|
370
|
+
},
|
|
371
|
+
customIdGenerator() {
|
|
372
|
+
return new ObjectId().toString();
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
adapter: createCustomAdapter(db)
|
|
376
|
+
};
|
|
377
|
+
lazyAdapter = createAdapterFactory(adapterOptions);
|
|
378
|
+
return (options) => {
|
|
379
|
+
lazyOptions = options;
|
|
380
|
+
return lazyAdapter(options);
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Safely escape user input for use in a MongoDB regex.
|
|
385
|
+
* This ensures the resulting pattern is treated as literal text,
|
|
386
|
+
* and not as a regex with special syntax.
|
|
387
|
+
*
|
|
388
|
+
* @param input - The input string to escape. Any type that isn't a string will be converted to an empty string.
|
|
389
|
+
* @param maxLength - The maximum length of the input string to escape. Defaults to 256. This is to prevent DOS attacks.
|
|
390
|
+
* @returns The escaped string.
|
|
391
|
+
*/
|
|
392
|
+
function escapeForMongoRegex(input, maxLength = 256) {
|
|
393
|
+
if (typeof input !== "string") return "";
|
|
394
|
+
return input.slice(0, maxLength).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
export { mongodbAdapter };
|
|
399
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/mongodb-adapter.ts"],"sourcesContent":["import type { BetterAuthOptions } from \"@better-auth/core\";\nimport type {\n\tAdapterFactoryCustomizeAdapterCreator,\n\tAdapterFactoryOptions,\n\tDBAdapter,\n\tDBAdapterDebugLogOption,\n\tWhere,\n} from \"@better-auth/core/db/adapter\";\nimport { createAdapterFactory } from \"@better-auth/core/db/adapter\";\nimport type { ClientSession, Db, MongoClient } from \"mongodb\";\nimport { ObjectId } from \"mongodb\";\n\nclass MongoAdapterError extends Error {\n\tconstructor(\n\t\tpublic code: \"INVALID_ID\" | \"UNSUPPORTED_OPERATOR\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MongoAdapterError\";\n\t}\n}\n\nexport interface MongoDBAdapterConfig {\n\t/**\n\t * MongoDB client instance\n\t * If not provided, Database transactions won't be enabled.\n\t */\n\tclient?: MongoClient | undefined;\n\t/**\n\t * Enable debug logs for the adapter\n\t *\n\t * @default false\n\t */\n\tdebugLogs?: DBAdapterDebugLogOption | undefined;\n\t/**\n\t * Use plural table names\n\t *\n\t * @default false\n\t */\n\tusePlural?: boolean | undefined;\n\t/**\n\t * Whether to execute multiple operations in a transaction.\n\t *\n\t * β οΈ Important:\n\t * - Defaults to `true` when a MongoDB client is provided.\n\t * - If your MongoDB instance does not support transactions\n\t * (e.g. standalone server without a replica set),\n\t * you must explicitly set `transaction: false`.\n\t */\n\ttransaction?: boolean | undefined;\n}\n\nexport const mongodbAdapter = (\n\tdb: Db,\n\tconfig?: MongoDBAdapterConfig | undefined,\n) => {\n\tlet lazyOptions: BetterAuthOptions | null;\n\n\tconst getCustomIdGenerator = (options: BetterAuthOptions) => {\n\t\tconst generator = options.advanced?.database?.generateId;\n\t\tif (typeof generator === \"function\") {\n\t\t\treturn generator;\n\t\t}\n\t\treturn undefined;\n\t};\n\n\tconst createCustomAdapter =\n\t\t(\n\t\t\tdb: Db,\n\t\t\tsession?: ClientSession | undefined,\n\t\t): AdapterFactoryCustomizeAdapterCreator =>\n\t\t({\n\t\t\tgetFieldAttributes,\n\t\t\tgetFieldName,\n\t\t\tschema,\n\t\t\tgetDefaultModelName,\n\t\t\toptions,\n\t\t}) => {\n\t\t\tconst customIdGen = getCustomIdGenerator(options);\n\n\t\t\tfunction serializeID({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tmodel,\n\t\t\t}: {\n\t\t\t\tfield: string;\n\t\t\t\tvalue: any;\n\t\t\t\tmodel: string;\n\t\t\t}) {\n\t\t\t\tif (customIdGen) {\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t\tmodel = getDefaultModelName(model);\n\t\t\t\tif (\n\t\t\t\t\tfield === \"id\" ||\n\t\t\t\t\tfield === \"_id\" ||\n\t\t\t\t\tschema[model]!.fields[field]?.references?.field === \"id\"\n\t\t\t\t) {\n\t\t\t\t\tif (value === null || value === undefined) {\n\t\t\t\t\t\treturn value;\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof value !== \"string\") {\n\t\t\t\t\t\tif (value instanceof ObjectId) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\t\t\treturn value.map((v) => {\n\t\t\t\t\t\t\t\tif (v === null || v === undefined) {\n\t\t\t\t\t\t\t\t\treturn v;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (typeof v === \"string\") {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\treturn new ObjectId(v);\n\t\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t\treturn v;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (v instanceof ObjectId) {\n\t\t\t\t\t\t\t\t\treturn v;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tthrow new MongoAdapterError(\"INVALID_ID\", \"Invalid id value\");\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new MongoAdapterError(\"INVALID_ID\", \"Invalid id value\");\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn new ObjectId(value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn value;\n\t\t\t}\n\n\t\t\tfunction convertWhereClause({\n\t\t\t\twhere,\n\t\t\t\tmodel,\n\t\t\t}: {\n\t\t\t\twhere: Where[];\n\t\t\t\tmodel: string;\n\t\t\t}) {\n\t\t\t\tif (!where.length) return {};\n\t\t\t\tconst conditions = where.map((w) => {\n\t\t\t\t\tconst {\n\t\t\t\t\t\tfield: field_,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\toperator = \"eq\",\n\t\t\t\t\t\tconnector = \"AND\",\n\t\t\t\t\t} = w;\n\t\t\t\t\tlet condition: any;\n\t\t\t\t\tlet field = getFieldName({ model, field: field_ });\n\t\t\t\t\tif (field === \"id\") field = \"_id\";\n\t\t\t\t\tswitch (operator.toLowerCase()) {\n\t\t\t\t\t\tcase \"eq\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: serializeID({\n\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"in\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$in: Array.isArray(value)\n\t\t\t\t\t\t\t\t\t\t? value.map((v) => serializeID({ field, value: v, model }))\n\t\t\t\t\t\t\t\t\t\t: [serializeID({ field, value, model })],\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"not_in\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$nin: Array.isArray(value)\n\t\t\t\t\t\t\t\t\t\t? value.map((v) => serializeID({ field, value: v, model }))\n\t\t\t\t\t\t\t\t\t\t: [serializeID({ field, value, model })],\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"gt\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$gt: serializeID({\n\t\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"gte\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$gte: serializeID({\n\t\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"lt\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$lt: serializeID({\n\t\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"lte\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$lte: serializeID({\n\t\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"ne\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$ne: serializeID({\n\t\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"contains\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: {\n\t\t\t\t\t\t\t\t\t$regex: `.*${escapeForMongoRegex(value as string)}.*`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"starts_with\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: { $regex: `^${escapeForMongoRegex(value as string)}` },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"ends_with\":\n\t\t\t\t\t\t\tcondition = {\n\t\t\t\t\t\t\t\t[field]: { $regex: `${escapeForMongoRegex(value as string)}$` },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tthrow new MongoAdapterError(\n\t\t\t\t\t\t\t\t\"UNSUPPORTED_OPERATOR\",\n\t\t\t\t\t\t\t\t`Unsupported operator: ${operator}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn { condition, connector };\n\t\t\t\t});\n\t\t\t\tif (conditions.length === 1) {\n\t\t\t\t\treturn conditions[0]!.condition;\n\t\t\t\t}\n\t\t\t\tconst andConditions = conditions\n\t\t\t\t\t.filter((c) => c.connector === \"AND\")\n\t\t\t\t\t.map((c) => c.condition);\n\t\t\t\tconst orConditions = conditions\n\t\t\t\t\t.filter((c) => c.connector === \"OR\")\n\t\t\t\t\t.map((c) => c.condition);\n\n\t\t\t\tlet clause = {};\n\t\t\t\tif (andConditions.length) {\n\t\t\t\t\tclause = { ...clause, $and: andConditions };\n\t\t\t\t}\n\t\t\t\tif (orConditions.length) {\n\t\t\t\t\tclause = { ...clause, $or: orConditions };\n\t\t\t\t}\n\t\t\t\treturn clause;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tasync create({ model, data: values }) {\n\t\t\t\t\tconst res = await db.collection(model).insertOne(values, { session });\n\t\t\t\t\tconst insertedData = { _id: res.insertedId.toString(), ...values };\n\t\t\t\t\treturn insertedData as any;\n\t\t\t\t},\n\t\t\t\tasync findOne({ model, where, select, join }) {\n\t\t\t\t\tconst matchStage = where\n\t\t\t\t\t\t? { $match: convertWhereClause({ where, model }) }\n\t\t\t\t\t\t: { $match: {} };\n\t\t\t\t\tconst pipeline: any[] = [matchStage];\n\n\t\t\t\t\tif (join) {\n\t\t\t\t\t\tfor (const [joinedModel, joinConfig] of Object.entries(join)) {\n\t\t\t\t\t\t\tconst localField = getFieldName({\n\t\t\t\t\t\t\t\tfield: joinConfig.on.from,\n\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tconst foreignField = getFieldName({\n\t\t\t\t\t\t\t\tfield: joinConfig.on.to,\n\t\t\t\t\t\t\t\tmodel: joinedModel,\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tconst localFieldName = localField === \"id\" ? \"_id\" : localField;\n\t\t\t\t\t\t\tconst foreignFieldName =\n\t\t\t\t\t\t\t\tforeignField === \"id\" ? \"_id\" : foreignField;\n\n\t\t\t\t\t\t\t// Only unwind if the foreign field has a unique constraint (one-to-one relationship)\n\t\t\t\t\t\t\tconst joinedModelSchema =\n\t\t\t\t\t\t\t\tschema[getDefaultModelName(joinedModel)];\n\t\t\t\t\t\t\tconst foreignFieldAttribute =\n\t\t\t\t\t\t\t\tjoinedModelSchema?.fields[joinConfig.on.to];\n\t\t\t\t\t\t\tconst isUnique = foreignFieldAttribute?.unique === true;\n\n\t\t\t\t\t\t\t// For unique relationships, limit is ignored (as per JoinConfig type)\n\t\t\t\t\t\t\t// For non-unique relationships, apply limit if specified\n\t\t\t\t\t\t\tconst shouldLimit = !isUnique && joinConfig.limit !== undefined;\n\t\t\t\t\t\t\tconst limit =\n\t\t\t\t\t\t\t\tjoinConfig.limit ??\n\t\t\t\t\t\t\t\toptions.advanced?.database?.defaultFindManyLimit ??\n\t\t\t\t\t\t\t\t100;\n\t\t\t\t\t\t\tif (shouldLimit && limit > 0) {\n\t\t\t\t\t\t\t\t// Use pipeline syntax to support limit\n\t\t\t\t\t\t\t\t// Construct the field reference string for the foreign field\n\t\t\t\t\t\t\t\tconst foreignFieldRef = `$${foreignFieldName}`;\n\t\t\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t\t\t$lookup: {\n\t\t\t\t\t\t\t\t\t\tfrom: joinedModel,\n\t\t\t\t\t\t\t\t\t\tlet: { localFieldValue: `$${localFieldName}` },\n\t\t\t\t\t\t\t\t\t\tpipeline: [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t$match: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$expr: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$eq: [foreignFieldRef, \"$$localFieldValue\"],\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t{ $limit: limit },\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\tas: joinedModel,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Use simple syntax when no limit is needed\n\t\t\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t\t\t$lookup: {\n\t\t\t\t\t\t\t\t\t\tfrom: joinedModel,\n\t\t\t\t\t\t\t\t\t\tlocalField: localFieldName,\n\t\t\t\t\t\t\t\t\t\tforeignField: foreignFieldName,\n\t\t\t\t\t\t\t\t\t\tas: joinedModel,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (isUnique) {\n\t\t\t\t\t\t\t\t// For one-to-one relationships, unwind to flatten to a single object\n\t\t\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t\t\t$unwind: {\n\t\t\t\t\t\t\t\t\t\tpath: `$${joinedModel}`,\n\t\t\t\t\t\t\t\t\t\tpreserveNullAndEmptyArrays: true,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// For one-to-many, keep as array - no unwind\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (select) {\n\t\t\t\t\t\tconst projection: any = {};\n\t\t\t\t\t\tselect.forEach((field) => {\n\t\t\t\t\t\t\tprojection[getFieldName({ field, model })] = 1;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Include joined collections in projection\n\t\t\t\t\t\tif (join) {\n\t\t\t\t\t\t\tfor (const joinedModel of Object.keys(join)) {\n\t\t\t\t\t\t\t\tprojection[joinedModel] = 1;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tpipeline.push({ $project: projection });\n\t\t\t\t\t}\n\n\t\t\t\t\tpipeline.push({ $limit: 1 });\n\n\t\t\t\t\tconst res = await db\n\t\t\t\t\t\t.collection(model)\n\t\t\t\t\t\t.aggregate(pipeline, { session })\n\t\t\t\t\t\t.toArray();\n\n\t\t\t\t\tif (!res || res.length === 0) return null;\n\t\t\t\t\treturn res[0] as any;\n\t\t\t\t},\n\t\t\t\tasync findMany({ model, where, limit, offset, sortBy, join }) {\n\t\t\t\t\tconst matchStage = where\n\t\t\t\t\t\t? { $match: convertWhereClause({ where, model }) }\n\t\t\t\t\t\t: { $match: {} };\n\t\t\t\t\tconst pipeline: any[] = [matchStage];\n\n\t\t\t\t\tif (join) {\n\t\t\t\t\t\tfor (const [joinedModel, joinConfig] of Object.entries(join)) {\n\t\t\t\t\t\t\tconst localField = getFieldName({\n\t\t\t\t\t\t\t\tfield: joinConfig.on.from,\n\t\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tconst foreignField = getFieldName({\n\t\t\t\t\t\t\t\tfield: joinConfig.on.to,\n\t\t\t\t\t\t\t\tmodel: joinedModel,\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tconst localFieldName = localField === \"id\" ? \"_id\" : localField;\n\t\t\t\t\t\t\tconst foreignFieldName =\n\t\t\t\t\t\t\t\tforeignField === \"id\" ? \"_id\" : foreignField;\n\n\t\t\t\t\t\t\t// Only unwind if the foreign field has a unique constraint (one-to-one relationship)\n\t\t\t\t\t\t\tconst foreignFieldAttribute = getFieldAttributes({\n\t\t\t\t\t\t\t\tmodel: joinedModel,\n\t\t\t\t\t\t\t\tfield: joinConfig.on.to,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tconst isUnique = foreignFieldAttribute?.unique === true;\n\n\t\t\t\t\t\t\t// For unique relationships, limit is ignored (as per JoinConfig type)\n\t\t\t\t\t\t\t// For non-unique relationships, apply limit if specified\n\t\t\t\t\t\t\tconst shouldLimit =\n\t\t\t\t\t\t\t\tjoinConfig.relation !== \"one-to-one\" &&\n\t\t\t\t\t\t\t\tjoinConfig.limit !== undefined;\n\n\t\t\t\t\t\t\tconst limit =\n\t\t\t\t\t\t\t\tjoinConfig.limit ??\n\t\t\t\t\t\t\t\toptions.advanced?.database?.defaultFindManyLimit ??\n\t\t\t\t\t\t\t\t100;\n\t\t\t\t\t\t\tif (shouldLimit && limit > 0) {\n\t\t\t\t\t\t\t\t// Use pipeline syntax to support limit\n\t\t\t\t\t\t\t\t// Construct the field reference string for the foreign field\n\t\t\t\t\t\t\t\tconst foreignFieldRef = `$${foreignFieldName}`;\n\t\t\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t\t\t$lookup: {\n\t\t\t\t\t\t\t\t\t\tfrom: joinedModel,\n\t\t\t\t\t\t\t\t\t\tlet: { localFieldValue: `$${localFieldName}` },\n\t\t\t\t\t\t\t\t\t\tpipeline: [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t$match: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$expr: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$eq: [foreignFieldRef, \"$$localFieldValue\"],\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t{ $limit: limit },\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\tas: joinedModel,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Use simple syntax when no limit is needed\n\t\t\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t\t\t$lookup: {\n\t\t\t\t\t\t\t\t\t\tfrom: joinedModel,\n\t\t\t\t\t\t\t\t\t\tlocalField: localFieldName,\n\t\t\t\t\t\t\t\t\t\tforeignField: foreignFieldName,\n\t\t\t\t\t\t\t\t\t\tas: joinedModel,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (isUnique) {\n\t\t\t\t\t\t\t\t// For one-to-one relationships, unwind to flatten to a single object\n\t\t\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t\t\t$unwind: {\n\t\t\t\t\t\t\t\t\t\tpath: `$${joinedModel}`,\n\t\t\t\t\t\t\t\t\t\tpreserveNullAndEmptyArrays: true,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// For one-to-many, keep as array - no unwind\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (sortBy) {\n\t\t\t\t\t\tpipeline.push({\n\t\t\t\t\t\t\t$sort: {\n\t\t\t\t\t\t\t\t[getFieldName({ field: sortBy.field, model })]:\n\t\t\t\t\t\t\t\t\tsortBy.direction === \"desc\" ? -1 : 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (offset) {\n\t\t\t\t\t\tpipeline.push({ $skip: offset });\n\t\t\t\t\t}\n\n\t\t\t\t\tif (limit) {\n\t\t\t\t\t\tpipeline.push({ $limit: limit });\n\t\t\t\t\t}\n\n\t\t\t\t\tconst res = await db\n\t\t\t\t\t\t.collection(model)\n\t\t\t\t\t\t.aggregate(pipeline, { session })\n\t\t\t\t\t\t.toArray();\n\n\t\t\t\t\treturn res as any;\n\t\t\t\t},\n\t\t\t\tasync count({ model, where }) {\n\t\t\t\t\tconst matchStage = where\n\t\t\t\t\t\t? { $match: convertWhereClause({ where, model }) }\n\t\t\t\t\t\t: { $match: {} };\n\t\t\t\t\tconst pipeline: any[] = [matchStage, { $count: \"total\" }];\n\n\t\t\t\t\tconst res = await db\n\t\t\t\t\t\t.collection(model)\n\t\t\t\t\t\t.aggregate(pipeline, { session })\n\t\t\t\t\t\t.toArray();\n\n\t\t\t\t\tif (!res || res.length === 0) return 0;\n\t\t\t\t\treturn res[0]?.total ?? 0;\n\t\t\t\t},\n\t\t\t\tasync update({ model, where, update: values }) {\n\t\t\t\t\tconst clause = convertWhereClause({ where, model });\n\n\t\t\t\t\tconst res = await db.collection(model).findOneAndUpdate(\n\t\t\t\t\t\tclause,\n\t\t\t\t\t\t{ $set: values as any },\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\treturnDocument: \"after\",\n\t\t\t\t\t\t\tincludeResultMetadata: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t\tconst doc = (res as any)?.value ?? null;\n\t\t\t\t\tif (!doc) return null;\n\t\t\t\t\treturn doc as any;\n\t\t\t\t},\n\t\t\t\tasync updateMany({ model, where, update: values }) {\n\t\t\t\t\tconst clause = convertWhereClause({ where, model });\n\n\t\t\t\t\tconst res = await db.collection(model).updateMany(\n\t\t\t\t\t\tclause,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$set: values as any,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ session },\n\t\t\t\t\t);\n\t\t\t\t\treturn res.modifiedCount;\n\t\t\t\t},\n\t\t\t\tasync delete({ model, where }) {\n\t\t\t\t\tconst clause = convertWhereClause({ where, model });\n\t\t\t\t\tawait db.collection(model).deleteOne(clause, { session });\n\t\t\t\t},\n\t\t\t\tasync deleteMany({ model, where }) {\n\t\t\t\t\tconst clause = convertWhereClause({ where, model });\n\t\t\t\t\tconst res = await db\n\t\t\t\t\t\t.collection(model)\n\t\t\t\t\t\t.deleteMany(clause, { session });\n\t\t\t\t\treturn res.deletedCount;\n\t\t\t\t},\n\t\t\t};\n\t\t};\n\n\tlet lazyAdapter:\n\t\t| ((options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>)\n\t\t| null = null;\n\tlet adapterOptions: AdapterFactoryOptions | null = null;\n\tadapterOptions = {\n\t\tconfig: {\n\t\t\tadapterId: \"mongodb-adapter\",\n\t\t\tadapterName: \"MongoDB Adapter\",\n\t\t\tusePlural: config?.usePlural ?? false,\n\t\t\tdebugLogs: config?.debugLogs ?? false,\n\t\t\tmapKeysTransformInput: {\n\t\t\t\tid: \"_id\",\n\t\t\t},\n\t\t\tmapKeysTransformOutput: {\n\t\t\t\t_id: \"id\",\n\t\t\t},\n\t\t\tsupportsArrays: true,\n\t\t\tsupportsNumericIds: false,\n\t\t\ttransaction:\n\t\t\t\tconfig?.client && (config?.transaction ?? true)\n\t\t\t\t\t? async (cb) => {\n\t\t\t\t\t\t\tif (!config.client) {\n\t\t\t\t\t\t\t\treturn cb(lazyAdapter!(lazyOptions!));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst session = config.client.startSession();\n\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tsession.startTransaction();\n\n\t\t\t\t\t\t\t\tconst adapter = createAdapterFactory({\n\t\t\t\t\t\t\t\t\tconfig: adapterOptions!.config,\n\t\t\t\t\t\t\t\t\tadapter: createCustomAdapter(db, session),\n\t\t\t\t\t\t\t\t})(lazyOptions!);\n\n\t\t\t\t\t\t\t\tconst result = await cb(adapter);\n\n\t\t\t\t\t\t\t\tawait session.commitTransaction();\n\t\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\tawait session.abortTransaction();\n\t\t\t\t\t\t\t\tthrow err;\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tawait session.endSession();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t: false,\n\t\t\tcustomTransformInput({\n\t\t\t\taction,\n\t\t\t\tdata,\n\t\t\t\tfield,\n\t\t\t\tfieldAttributes,\n\t\t\t\tschema,\n\t\t\t\tmodel,\n\t\t\t\toptions,\n\t\t\t}) {\n\t\t\t\tconst customIdGen = getCustomIdGenerator(options);\n\t\t\t\tif (field === \"_id\" || fieldAttributes.references?.field === \"id\") {\n\t\t\t\t\tif (customIdGen) {\n\t\t\t\t\t\treturn data;\n\t\t\t\t\t}\n\t\t\t\t\tif (action !== \"create\") {\n\t\t\t\t\t\treturn data;\n\t\t\t\t\t}\n\t\t\t\t\tif (Array.isArray(data)) {\n\t\t\t\t\t\treturn data.map((v) => {\n\t\t\t\t\t\t\tif (typeof v === \"string\") {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst oid = new ObjectId(v);\n\t\t\t\t\t\t\t\t\treturn oid;\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\treturn v;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn v;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof data === \"string\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst oid = new ObjectId(data);\n\t\t\t\t\t\t\treturn oid;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn data;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (\n\t\t\t\t\t\tfieldAttributes?.references?.field === \"id\" &&\n\t\t\t\t\t\t!fieldAttributes?.required &&\n\t\t\t\t\t\tdata === null\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tconst oid = new ObjectId();\n\t\t\t\t\treturn oid;\n\t\t\t\t}\n\t\t\t\treturn data;\n\t\t\t},\n\t\t\tcustomTransformOutput({ data, field, fieldAttributes }) {\n\t\t\t\tif (field === \"id\" || fieldAttributes.references?.field === \"id\") {\n\t\t\t\t\tif (data instanceof ObjectId) {\n\t\t\t\t\t\treturn data.toHexString();\n\t\t\t\t\t}\n\t\t\t\t\tif (Array.isArray(data)) {\n\t\t\t\t\t\treturn data.map((v) => {\n\t\t\t\t\t\t\tif (v instanceof ObjectId) {\n\t\t\t\t\t\t\t\treturn v.toHexString();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn v;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\t\t\t\treturn data;\n\t\t\t},\n\t\t\tcustomIdGenerator() {\n\t\t\t\treturn new ObjectId().toString();\n\t\t\t},\n\t\t},\n\t\tadapter: createCustomAdapter(db),\n\t};\n\tlazyAdapter = createAdapterFactory(adapterOptions);\n\n\treturn (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {\n\t\tlazyOptions = options;\n\t\treturn lazyAdapter(options);\n\t};\n};\n\n/**\n * Safely escape user input for use in a MongoDB regex.\n * This ensures the resulting pattern is treated as literal text,\n * and not as a regex with special syntax.\n *\n * @param input - The input string to escape. Any type that isn't a string will be converted to an empty string.\n * @param maxLength - The maximum length of the input string to escape. Defaults to 256. This is to prevent DOS attacks.\n * @returns The escaped string.\n */\nfunction escapeForMongoRegex(input: string, maxLength = 256): string {\n\tif (typeof input !== \"string\") return \"\";\n\n\t// Escape all PCRE special characters\n\t// Source: PCRE docs β https://www.pcre.org/original/doc/html/pcrepattern.html\n\treturn input.slice(0, maxLength).replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hammadj/better-auth-mongo-adapter",
|
|
3
|
+
"version": "1.5.0-beta.9",
|
|
4
|
+
"description": "Mongo adapter for Better Auth",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/META-DREAMER/better-auth.git",
|
|
9
|
+
"directory": "packages/mongo-adapter"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.mjs",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"dev-source": "./src/index.ts",
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@better-auth/utils": "^0.3.0",
|
|
23
|
+
"mongodb": "^6.0.0 || ^7.0.0",
|
|
24
|
+
"@hammadj/better-auth-core": "1.5.0-beta.9"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@better-auth/utils": "^0.3.1",
|
|
28
|
+
"mongodb": "^7.0.0",
|
|
29
|
+
"tsdown": "^0.20.1",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"@hammadj/better-auth-core": "1.5.0-beta.9"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsdown",
|
|
35
|
+
"dev": "tsdown --watch",
|
|
36
|
+
"test": "vitest",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mongodb-adapter";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mongodbAdapter } from "./mongodb-adapter";
|
|
3
|
+
|
|
4
|
+
describe("mongodb-adapter", () => {
|
|
5
|
+
it("should create mongodb adapter", () => {
|
|
6
|
+
const db = {
|
|
7
|
+
collection: vi.fn(),
|
|
8
|
+
} as any;
|
|
9
|
+
const adapter = mongodbAdapter(db);
|
|
10
|
+
expect(adapter).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import type { BetterAuthOptions } from "@better-auth/core";
|
|
2
|
+
import type {
|
|
3
|
+
AdapterFactoryCustomizeAdapterCreator,
|
|
4
|
+
AdapterFactoryOptions,
|
|
5
|
+
DBAdapter,
|
|
6
|
+
DBAdapterDebugLogOption,
|
|
7
|
+
Where,
|
|
8
|
+
} from "@better-auth/core/db/adapter";
|
|
9
|
+
import { createAdapterFactory } from "@better-auth/core/db/adapter";
|
|
10
|
+
import type { ClientSession, Db, MongoClient } from "mongodb";
|
|
11
|
+
import { ObjectId } from "mongodb";
|
|
12
|
+
|
|
13
|
+
class MongoAdapterError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
public code: "INVALID_ID" | "UNSUPPORTED_OPERATOR",
|
|
16
|
+
message: string,
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "MongoAdapterError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MongoDBAdapterConfig {
|
|
24
|
+
/**
|
|
25
|
+
* MongoDB client instance
|
|
26
|
+
* If not provided, Database transactions won't be enabled.
|
|
27
|
+
*/
|
|
28
|
+
client?: MongoClient | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Enable debug logs for the adapter
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
debugLogs?: DBAdapterDebugLogOption | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Use plural table names
|
|
37
|
+
*
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
usePlural?: boolean | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Whether to execute multiple operations in a transaction.
|
|
43
|
+
*
|
|
44
|
+
* β οΈ Important:
|
|
45
|
+
* - Defaults to `true` when a MongoDB client is provided.
|
|
46
|
+
* - If your MongoDB instance does not support transactions
|
|
47
|
+
* (e.g. standalone server without a replica set),
|
|
48
|
+
* you must explicitly set `transaction: false`.
|
|
49
|
+
*/
|
|
50
|
+
transaction?: boolean | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const mongodbAdapter = (
|
|
54
|
+
db: Db,
|
|
55
|
+
config?: MongoDBAdapterConfig | undefined,
|
|
56
|
+
) => {
|
|
57
|
+
let lazyOptions: BetterAuthOptions | null;
|
|
58
|
+
|
|
59
|
+
const getCustomIdGenerator = (options: BetterAuthOptions) => {
|
|
60
|
+
const generator = options.advanced?.database?.generateId;
|
|
61
|
+
if (typeof generator === "function") {
|
|
62
|
+
return generator;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const createCustomAdapter =
|
|
68
|
+
(
|
|
69
|
+
db: Db,
|
|
70
|
+
session?: ClientSession | undefined,
|
|
71
|
+
): AdapterFactoryCustomizeAdapterCreator =>
|
|
72
|
+
({
|
|
73
|
+
getFieldAttributes,
|
|
74
|
+
getFieldName,
|
|
75
|
+
schema,
|
|
76
|
+
getDefaultModelName,
|
|
77
|
+
options,
|
|
78
|
+
}) => {
|
|
79
|
+
const customIdGen = getCustomIdGenerator(options);
|
|
80
|
+
|
|
81
|
+
function serializeID({
|
|
82
|
+
field,
|
|
83
|
+
value,
|
|
84
|
+
model,
|
|
85
|
+
}: {
|
|
86
|
+
field: string;
|
|
87
|
+
value: any;
|
|
88
|
+
model: string;
|
|
89
|
+
}) {
|
|
90
|
+
if (customIdGen) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
model = getDefaultModelName(model);
|
|
94
|
+
if (
|
|
95
|
+
field === "id" ||
|
|
96
|
+
field === "_id" ||
|
|
97
|
+
schema[model]!.fields[field]?.references?.field === "id"
|
|
98
|
+
) {
|
|
99
|
+
if (value === null || value === undefined) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
if (typeof value !== "string") {
|
|
103
|
+
if (value instanceof ObjectId) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
return value.map((v) => {
|
|
108
|
+
if (v === null || v === undefined) {
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
111
|
+
if (typeof v === "string") {
|
|
112
|
+
try {
|
|
113
|
+
return new ObjectId(v);
|
|
114
|
+
} catch {
|
|
115
|
+
return v;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (v instanceof ObjectId) {
|
|
119
|
+
return v;
|
|
120
|
+
}
|
|
121
|
+
throw new MongoAdapterError("INVALID_ID", "Invalid id value");
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
throw new MongoAdapterError("INVALID_ID", "Invalid id value");
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return new ObjectId(value);
|
|
128
|
+
} catch {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function convertWhereClause({
|
|
136
|
+
where,
|
|
137
|
+
model,
|
|
138
|
+
}: {
|
|
139
|
+
where: Where[];
|
|
140
|
+
model: string;
|
|
141
|
+
}) {
|
|
142
|
+
if (!where.length) return {};
|
|
143
|
+
const conditions = where.map((w) => {
|
|
144
|
+
const {
|
|
145
|
+
field: field_,
|
|
146
|
+
value,
|
|
147
|
+
operator = "eq",
|
|
148
|
+
connector = "AND",
|
|
149
|
+
} = w;
|
|
150
|
+
let condition: any;
|
|
151
|
+
let field = getFieldName({ model, field: field_ });
|
|
152
|
+
if (field === "id") field = "_id";
|
|
153
|
+
switch (operator.toLowerCase()) {
|
|
154
|
+
case "eq":
|
|
155
|
+
condition = {
|
|
156
|
+
[field]: serializeID({
|
|
157
|
+
field,
|
|
158
|
+
value,
|
|
159
|
+
model,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
break;
|
|
163
|
+
case "in":
|
|
164
|
+
condition = {
|
|
165
|
+
[field]: {
|
|
166
|
+
$in: Array.isArray(value)
|
|
167
|
+
? value.map((v) => serializeID({ field, value: v, model }))
|
|
168
|
+
: [serializeID({ field, value, model })],
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
break;
|
|
172
|
+
case "not_in":
|
|
173
|
+
condition = {
|
|
174
|
+
[field]: {
|
|
175
|
+
$nin: Array.isArray(value)
|
|
176
|
+
? value.map((v) => serializeID({ field, value: v, model }))
|
|
177
|
+
: [serializeID({ field, value, model })],
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
break;
|
|
181
|
+
case "gt":
|
|
182
|
+
condition = {
|
|
183
|
+
[field]: {
|
|
184
|
+
$gt: serializeID({
|
|
185
|
+
field,
|
|
186
|
+
value,
|
|
187
|
+
model,
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
break;
|
|
192
|
+
case "gte":
|
|
193
|
+
condition = {
|
|
194
|
+
[field]: {
|
|
195
|
+
$gte: serializeID({
|
|
196
|
+
field,
|
|
197
|
+
value,
|
|
198
|
+
model,
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
break;
|
|
203
|
+
case "lt":
|
|
204
|
+
condition = {
|
|
205
|
+
[field]: {
|
|
206
|
+
$lt: serializeID({
|
|
207
|
+
field,
|
|
208
|
+
value,
|
|
209
|
+
model,
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
break;
|
|
214
|
+
case "lte":
|
|
215
|
+
condition = {
|
|
216
|
+
[field]: {
|
|
217
|
+
$lte: serializeID({
|
|
218
|
+
field,
|
|
219
|
+
value,
|
|
220
|
+
model,
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
break;
|
|
225
|
+
case "ne":
|
|
226
|
+
condition = {
|
|
227
|
+
[field]: {
|
|
228
|
+
$ne: serializeID({
|
|
229
|
+
field,
|
|
230
|
+
value,
|
|
231
|
+
model,
|
|
232
|
+
}),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
break;
|
|
236
|
+
case "contains":
|
|
237
|
+
condition = {
|
|
238
|
+
[field]: {
|
|
239
|
+
$regex: `.*${escapeForMongoRegex(value as string)}.*`,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
break;
|
|
243
|
+
case "starts_with":
|
|
244
|
+
condition = {
|
|
245
|
+
[field]: { $regex: `^${escapeForMongoRegex(value as string)}` },
|
|
246
|
+
};
|
|
247
|
+
break;
|
|
248
|
+
case "ends_with":
|
|
249
|
+
condition = {
|
|
250
|
+
[field]: { $regex: `${escapeForMongoRegex(value as string)}$` },
|
|
251
|
+
};
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
throw new MongoAdapterError(
|
|
255
|
+
"UNSUPPORTED_OPERATOR",
|
|
256
|
+
`Unsupported operator: ${operator}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return { condition, connector };
|
|
260
|
+
});
|
|
261
|
+
if (conditions.length === 1) {
|
|
262
|
+
return conditions[0]!.condition;
|
|
263
|
+
}
|
|
264
|
+
const andConditions = conditions
|
|
265
|
+
.filter((c) => c.connector === "AND")
|
|
266
|
+
.map((c) => c.condition);
|
|
267
|
+
const orConditions = conditions
|
|
268
|
+
.filter((c) => c.connector === "OR")
|
|
269
|
+
.map((c) => c.condition);
|
|
270
|
+
|
|
271
|
+
let clause = {};
|
|
272
|
+
if (andConditions.length) {
|
|
273
|
+
clause = { ...clause, $and: andConditions };
|
|
274
|
+
}
|
|
275
|
+
if (orConditions.length) {
|
|
276
|
+
clause = { ...clause, $or: orConditions };
|
|
277
|
+
}
|
|
278
|
+
return clause;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
async create({ model, data: values }) {
|
|
283
|
+
const res = await db.collection(model).insertOne(values, { session });
|
|
284
|
+
const insertedData = { _id: res.insertedId.toString(), ...values };
|
|
285
|
+
return insertedData as any;
|
|
286
|
+
},
|
|
287
|
+
async findOne({ model, where, select, join }) {
|
|
288
|
+
const matchStage = where
|
|
289
|
+
? { $match: convertWhereClause({ where, model }) }
|
|
290
|
+
: { $match: {} };
|
|
291
|
+
const pipeline: any[] = [matchStage];
|
|
292
|
+
|
|
293
|
+
if (join) {
|
|
294
|
+
for (const [joinedModel, joinConfig] of Object.entries(join)) {
|
|
295
|
+
const localField = getFieldName({
|
|
296
|
+
field: joinConfig.on.from,
|
|
297
|
+
model,
|
|
298
|
+
});
|
|
299
|
+
const foreignField = getFieldName({
|
|
300
|
+
field: joinConfig.on.to,
|
|
301
|
+
model: joinedModel,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const localFieldName = localField === "id" ? "_id" : localField;
|
|
305
|
+
const foreignFieldName =
|
|
306
|
+
foreignField === "id" ? "_id" : foreignField;
|
|
307
|
+
|
|
308
|
+
// Only unwind if the foreign field has a unique constraint (one-to-one relationship)
|
|
309
|
+
const joinedModelSchema =
|
|
310
|
+
schema[getDefaultModelName(joinedModel)];
|
|
311
|
+
const foreignFieldAttribute =
|
|
312
|
+
joinedModelSchema?.fields[joinConfig.on.to];
|
|
313
|
+
const isUnique = foreignFieldAttribute?.unique === true;
|
|
314
|
+
|
|
315
|
+
// For unique relationships, limit is ignored (as per JoinConfig type)
|
|
316
|
+
// For non-unique relationships, apply limit if specified
|
|
317
|
+
const shouldLimit = !isUnique && joinConfig.limit !== undefined;
|
|
318
|
+
const limit =
|
|
319
|
+
joinConfig.limit ??
|
|
320
|
+
options.advanced?.database?.defaultFindManyLimit ??
|
|
321
|
+
100;
|
|
322
|
+
if (shouldLimit && limit > 0) {
|
|
323
|
+
// Use pipeline syntax to support limit
|
|
324
|
+
// Construct the field reference string for the foreign field
|
|
325
|
+
const foreignFieldRef = `$${foreignFieldName}`;
|
|
326
|
+
pipeline.push({
|
|
327
|
+
$lookup: {
|
|
328
|
+
from: joinedModel,
|
|
329
|
+
let: { localFieldValue: `$${localFieldName}` },
|
|
330
|
+
pipeline: [
|
|
331
|
+
{
|
|
332
|
+
$match: {
|
|
333
|
+
$expr: {
|
|
334
|
+
$eq: [foreignFieldRef, "$$localFieldValue"],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{ $limit: limit },
|
|
339
|
+
],
|
|
340
|
+
as: joinedModel,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
// Use simple syntax when no limit is needed
|
|
345
|
+
pipeline.push({
|
|
346
|
+
$lookup: {
|
|
347
|
+
from: joinedModel,
|
|
348
|
+
localField: localFieldName,
|
|
349
|
+
foreignField: foreignFieldName,
|
|
350
|
+
as: joinedModel,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (isUnique) {
|
|
356
|
+
// For one-to-one relationships, unwind to flatten to a single object
|
|
357
|
+
pipeline.push({
|
|
358
|
+
$unwind: {
|
|
359
|
+
path: `$${joinedModel}`,
|
|
360
|
+
preserveNullAndEmptyArrays: true,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// For one-to-many, keep as array - no unwind
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (select) {
|
|
369
|
+
const projection: any = {};
|
|
370
|
+
select.forEach((field) => {
|
|
371
|
+
projection[getFieldName({ field, model })] = 1;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Include joined collections in projection
|
|
375
|
+
if (join) {
|
|
376
|
+
for (const joinedModel of Object.keys(join)) {
|
|
377
|
+
projection[joinedModel] = 1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
pipeline.push({ $project: projection });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
pipeline.push({ $limit: 1 });
|
|
385
|
+
|
|
386
|
+
const res = await db
|
|
387
|
+
.collection(model)
|
|
388
|
+
.aggregate(pipeline, { session })
|
|
389
|
+
.toArray();
|
|
390
|
+
|
|
391
|
+
if (!res || res.length === 0) return null;
|
|
392
|
+
return res[0] as any;
|
|
393
|
+
},
|
|
394
|
+
async findMany({ model, where, limit, offset, sortBy, join }) {
|
|
395
|
+
const matchStage = where
|
|
396
|
+
? { $match: convertWhereClause({ where, model }) }
|
|
397
|
+
: { $match: {} };
|
|
398
|
+
const pipeline: any[] = [matchStage];
|
|
399
|
+
|
|
400
|
+
if (join) {
|
|
401
|
+
for (const [joinedModel, joinConfig] of Object.entries(join)) {
|
|
402
|
+
const localField = getFieldName({
|
|
403
|
+
field: joinConfig.on.from,
|
|
404
|
+
model,
|
|
405
|
+
});
|
|
406
|
+
const foreignField = getFieldName({
|
|
407
|
+
field: joinConfig.on.to,
|
|
408
|
+
model: joinedModel,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const localFieldName = localField === "id" ? "_id" : localField;
|
|
412
|
+
const foreignFieldName =
|
|
413
|
+
foreignField === "id" ? "_id" : foreignField;
|
|
414
|
+
|
|
415
|
+
// Only unwind if the foreign field has a unique constraint (one-to-one relationship)
|
|
416
|
+
const foreignFieldAttribute = getFieldAttributes({
|
|
417
|
+
model: joinedModel,
|
|
418
|
+
field: joinConfig.on.to,
|
|
419
|
+
});
|
|
420
|
+
const isUnique = foreignFieldAttribute?.unique === true;
|
|
421
|
+
|
|
422
|
+
// For unique relationships, limit is ignored (as per JoinConfig type)
|
|
423
|
+
// For non-unique relationships, apply limit if specified
|
|
424
|
+
const shouldLimit =
|
|
425
|
+
joinConfig.relation !== "one-to-one" &&
|
|
426
|
+
joinConfig.limit !== undefined;
|
|
427
|
+
|
|
428
|
+
const limit =
|
|
429
|
+
joinConfig.limit ??
|
|
430
|
+
options.advanced?.database?.defaultFindManyLimit ??
|
|
431
|
+
100;
|
|
432
|
+
if (shouldLimit && limit > 0) {
|
|
433
|
+
// Use pipeline syntax to support limit
|
|
434
|
+
// Construct the field reference string for the foreign field
|
|
435
|
+
const foreignFieldRef = `$${foreignFieldName}`;
|
|
436
|
+
pipeline.push({
|
|
437
|
+
$lookup: {
|
|
438
|
+
from: joinedModel,
|
|
439
|
+
let: { localFieldValue: `$${localFieldName}` },
|
|
440
|
+
pipeline: [
|
|
441
|
+
{
|
|
442
|
+
$match: {
|
|
443
|
+
$expr: {
|
|
444
|
+
$eq: [foreignFieldRef, "$$localFieldValue"],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{ $limit: limit },
|
|
449
|
+
],
|
|
450
|
+
as: joinedModel,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
// Use simple syntax when no limit is needed
|
|
455
|
+
pipeline.push({
|
|
456
|
+
$lookup: {
|
|
457
|
+
from: joinedModel,
|
|
458
|
+
localField: localFieldName,
|
|
459
|
+
foreignField: foreignFieldName,
|
|
460
|
+
as: joinedModel,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (isUnique) {
|
|
466
|
+
// For one-to-one relationships, unwind to flatten to a single object
|
|
467
|
+
pipeline.push({
|
|
468
|
+
$unwind: {
|
|
469
|
+
path: `$${joinedModel}`,
|
|
470
|
+
preserveNullAndEmptyArrays: true,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
// For one-to-many, keep as array - no unwind
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (sortBy) {
|
|
479
|
+
pipeline.push({
|
|
480
|
+
$sort: {
|
|
481
|
+
[getFieldName({ field: sortBy.field, model })]:
|
|
482
|
+
sortBy.direction === "desc" ? -1 : 1,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (offset) {
|
|
488
|
+
pipeline.push({ $skip: offset });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (limit) {
|
|
492
|
+
pipeline.push({ $limit: limit });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const res = await db
|
|
496
|
+
.collection(model)
|
|
497
|
+
.aggregate(pipeline, { session })
|
|
498
|
+
.toArray();
|
|
499
|
+
|
|
500
|
+
return res as any;
|
|
501
|
+
},
|
|
502
|
+
async count({ model, where }) {
|
|
503
|
+
const matchStage = where
|
|
504
|
+
? { $match: convertWhereClause({ where, model }) }
|
|
505
|
+
: { $match: {} };
|
|
506
|
+
const pipeline: any[] = [matchStage, { $count: "total" }];
|
|
507
|
+
|
|
508
|
+
const res = await db
|
|
509
|
+
.collection(model)
|
|
510
|
+
.aggregate(pipeline, { session })
|
|
511
|
+
.toArray();
|
|
512
|
+
|
|
513
|
+
if (!res || res.length === 0) return 0;
|
|
514
|
+
return res[0]?.total ?? 0;
|
|
515
|
+
},
|
|
516
|
+
async update({ model, where, update: values }) {
|
|
517
|
+
const clause = convertWhereClause({ where, model });
|
|
518
|
+
|
|
519
|
+
const res = await db.collection(model).findOneAndUpdate(
|
|
520
|
+
clause,
|
|
521
|
+
{ $set: values as any },
|
|
522
|
+
{
|
|
523
|
+
session,
|
|
524
|
+
returnDocument: "after",
|
|
525
|
+
includeResultMetadata: true,
|
|
526
|
+
},
|
|
527
|
+
);
|
|
528
|
+
const doc = (res as any)?.value ?? null;
|
|
529
|
+
if (!doc) return null;
|
|
530
|
+
return doc as any;
|
|
531
|
+
},
|
|
532
|
+
async updateMany({ model, where, update: values }) {
|
|
533
|
+
const clause = convertWhereClause({ where, model });
|
|
534
|
+
|
|
535
|
+
const res = await db.collection(model).updateMany(
|
|
536
|
+
clause,
|
|
537
|
+
{
|
|
538
|
+
$set: values as any,
|
|
539
|
+
},
|
|
540
|
+
{ session },
|
|
541
|
+
);
|
|
542
|
+
return res.modifiedCount;
|
|
543
|
+
},
|
|
544
|
+
async delete({ model, where }) {
|
|
545
|
+
const clause = convertWhereClause({ where, model });
|
|
546
|
+
await db.collection(model).deleteOne(clause, { session });
|
|
547
|
+
},
|
|
548
|
+
async deleteMany({ model, where }) {
|
|
549
|
+
const clause = convertWhereClause({ where, model });
|
|
550
|
+
const res = await db
|
|
551
|
+
.collection(model)
|
|
552
|
+
.deleteMany(clause, { session });
|
|
553
|
+
return res.deletedCount;
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
let lazyAdapter:
|
|
559
|
+
| ((options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>)
|
|
560
|
+
| null = null;
|
|
561
|
+
let adapterOptions: AdapterFactoryOptions | null = null;
|
|
562
|
+
adapterOptions = {
|
|
563
|
+
config: {
|
|
564
|
+
adapterId: "mongodb-adapter",
|
|
565
|
+
adapterName: "MongoDB Adapter",
|
|
566
|
+
usePlural: config?.usePlural ?? false,
|
|
567
|
+
debugLogs: config?.debugLogs ?? false,
|
|
568
|
+
mapKeysTransformInput: {
|
|
569
|
+
id: "_id",
|
|
570
|
+
},
|
|
571
|
+
mapKeysTransformOutput: {
|
|
572
|
+
_id: "id",
|
|
573
|
+
},
|
|
574
|
+
supportsArrays: true,
|
|
575
|
+
supportsNumericIds: false,
|
|
576
|
+
transaction:
|
|
577
|
+
config?.client && (config?.transaction ?? true)
|
|
578
|
+
? async (cb) => {
|
|
579
|
+
if (!config.client) {
|
|
580
|
+
return cb(lazyAdapter!(lazyOptions!));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const session = config.client.startSession();
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
session.startTransaction();
|
|
587
|
+
|
|
588
|
+
const adapter = createAdapterFactory({
|
|
589
|
+
config: adapterOptions!.config,
|
|
590
|
+
adapter: createCustomAdapter(db, session),
|
|
591
|
+
})(lazyOptions!);
|
|
592
|
+
|
|
593
|
+
const result = await cb(adapter);
|
|
594
|
+
|
|
595
|
+
await session.commitTransaction();
|
|
596
|
+
return result;
|
|
597
|
+
} catch (err) {
|
|
598
|
+
await session.abortTransaction();
|
|
599
|
+
throw err;
|
|
600
|
+
} finally {
|
|
601
|
+
await session.endSession();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
: false,
|
|
605
|
+
customTransformInput({
|
|
606
|
+
action,
|
|
607
|
+
data,
|
|
608
|
+
field,
|
|
609
|
+
fieldAttributes,
|
|
610
|
+
schema,
|
|
611
|
+
model,
|
|
612
|
+
options,
|
|
613
|
+
}) {
|
|
614
|
+
const customIdGen = getCustomIdGenerator(options);
|
|
615
|
+
if (field === "_id" || fieldAttributes.references?.field === "id") {
|
|
616
|
+
if (customIdGen) {
|
|
617
|
+
return data;
|
|
618
|
+
}
|
|
619
|
+
if (action !== "create") {
|
|
620
|
+
return data;
|
|
621
|
+
}
|
|
622
|
+
if (Array.isArray(data)) {
|
|
623
|
+
return data.map((v) => {
|
|
624
|
+
if (typeof v === "string") {
|
|
625
|
+
try {
|
|
626
|
+
const oid = new ObjectId(v);
|
|
627
|
+
return oid;
|
|
628
|
+
} catch {
|
|
629
|
+
return v;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return v;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (typeof data === "string") {
|
|
636
|
+
try {
|
|
637
|
+
const oid = new ObjectId(data);
|
|
638
|
+
return oid;
|
|
639
|
+
} catch {
|
|
640
|
+
return data;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (
|
|
644
|
+
fieldAttributes?.references?.field === "id" &&
|
|
645
|
+
!fieldAttributes?.required &&
|
|
646
|
+
data === null
|
|
647
|
+
) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const oid = new ObjectId();
|
|
651
|
+
return oid;
|
|
652
|
+
}
|
|
653
|
+
return data;
|
|
654
|
+
},
|
|
655
|
+
customTransformOutput({ data, field, fieldAttributes }) {
|
|
656
|
+
if (field === "id" || fieldAttributes.references?.field === "id") {
|
|
657
|
+
if (data instanceof ObjectId) {
|
|
658
|
+
return data.toHexString();
|
|
659
|
+
}
|
|
660
|
+
if (Array.isArray(data)) {
|
|
661
|
+
return data.map((v) => {
|
|
662
|
+
if (v instanceof ObjectId) {
|
|
663
|
+
return v.toHexString();
|
|
664
|
+
}
|
|
665
|
+
return v;
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return data;
|
|
669
|
+
}
|
|
670
|
+
return data;
|
|
671
|
+
},
|
|
672
|
+
customIdGenerator() {
|
|
673
|
+
return new ObjectId().toString();
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
adapter: createCustomAdapter(db),
|
|
677
|
+
};
|
|
678
|
+
lazyAdapter = createAdapterFactory(adapterOptions);
|
|
679
|
+
|
|
680
|
+
return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
|
|
681
|
+
lazyOptions = options;
|
|
682
|
+
return lazyAdapter(options);
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Safely escape user input for use in a MongoDB regex.
|
|
688
|
+
* This ensures the resulting pattern is treated as literal text,
|
|
689
|
+
* and not as a regex with special syntax.
|
|
690
|
+
*
|
|
691
|
+
* @param input - The input string to escape. Any type that isn't a string will be converted to an empty string.
|
|
692
|
+
* @param maxLength - The maximum length of the input string to escape. Defaults to 256. This is to prevent DOS attacks.
|
|
693
|
+
* @returns The escaped string.
|
|
694
|
+
*/
|
|
695
|
+
function escapeForMongoRegex(input: string, maxLength = 256): string {
|
|
696
|
+
if (typeof input !== "string") return "";
|
|
697
|
+
|
|
698
|
+
// Escape all PCRE special characters
|
|
699
|
+
// Source: PCRE docs β https://www.pcre.org/original/doc/html/pcrepattern.html
|
|
700
|
+
return input.slice(0, maxLength).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
701
|
+
}
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
package/vitest.config.ts
ADDED