@better-auth/prisma-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.
@@ -0,0 +1,13 @@
1
+
2
+ > @better-auth/prisma-adapter@1.5.0-beta.9 build /home/runner/work/better-auth/better-auth/packages/prisma-adapter
3
+ > tsdown
4
+
5
+ ℹ tsdown v0.19.0 powered by rolldown v1.0.0-beta.59
6
+ ℹ config file: /home/runner/work/better-auth/better-auth/packages/prisma-adapter/tsdown.config.ts
7
+ ℹ entry: src/index.ts
8
+ ℹ tsconfig: tsconfig.json
9
+ ℹ Build start
10
+ ℹ dist/index.mjs 11.33 kB │ gzip: 2.66 kB
11
+ ℹ dist/index.d.mts  1.03 kB │ gzip: 0.48 kB
12
+ ℹ 2 files, total: 12.36 kB
13
+ ✔ Build complete in 8214ms
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.
@@ -0,0 +1,34 @@
1
+ import { DBAdapter, DBAdapterDebugLogOption } from "@better-auth/core/db/adapter";
2
+ import { BetterAuthOptions } from "@better-auth/core";
3
+
4
+ //#region src/prisma-adapter.d.ts
5
+ interface PrismaConfig {
6
+ /**
7
+ * Database provider.
8
+ */
9
+ provider: "sqlite" | "cockroachdb" | "mysql" | "postgresql" | "sqlserver" | "mongodb";
10
+ /**
11
+ * Enable debug logs for the adapter
12
+ *
13
+ * @default false
14
+ */
15
+ debugLogs?: DBAdapterDebugLogOption | undefined;
16
+ /**
17
+ * Use plural table names
18
+ *
19
+ * @default false
20
+ */
21
+ usePlural?: boolean | undefined;
22
+ /**
23
+ * Whether to execute multiple operations in a transaction.
24
+ *
25
+ * If the database doesn't support transactions,
26
+ * set this to `false` and operations will be executed sequentially.
27
+ * @default false
28
+ */
29
+ transaction?: boolean | undefined;
30
+ }
31
+ interface PrismaClient {}
32
+ declare const prismaAdapter: (prisma: PrismaClient, config: PrismaConfig) => (options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>;
33
+ //#endregion
34
+ export { PrismaConfig, prismaAdapter };
package/dist/index.mjs ADDED
@@ -0,0 +1,285 @@
1
+ import { createAdapterFactory } from "@better-auth/core/db/adapter";
2
+ import { BetterAuthError } from "@better-auth/core/error";
3
+
4
+ //#region src/prisma-adapter.ts
5
+ const prismaAdapter = (prisma, config) => {
6
+ let lazyOptions = null;
7
+ const createCustomAdapter = (prisma$1) => ({ getFieldName, getModelName, getFieldAttributes, getDefaultModelName, schema }) => {
8
+ const db = prisma$1;
9
+ const convertSelect = (select, model, join) => {
10
+ if (!select && !join) return void 0;
11
+ const result = {};
12
+ if (select) for (const field of select) result[getFieldName({
13
+ model,
14
+ field
15
+ })] = true;
16
+ if (join) {
17
+ if (!select) {
18
+ const fields = schema[getDefaultModelName(model)]?.fields || {};
19
+ fields.id = { type: "string" };
20
+ for (const field of Object.keys(fields)) result[getFieldName({
21
+ model,
22
+ field
23
+ })] = true;
24
+ }
25
+ for (const [joinModel, joinAttr] of Object.entries(join)) {
26
+ const key = getJoinKeyName(model, getModelName(joinModel), schema);
27
+ if (joinAttr.relation === "one-to-one") result[key] = true;
28
+ else result[key] = { take: joinAttr.limit };
29
+ }
30
+ }
31
+ return result;
32
+ };
33
+ /**
34
+ * Build the join key name based on whether the foreign field is unique or not.
35
+ * If unique, use singular. Otherwise, pluralize (add 's').
36
+ */
37
+ const getJoinKeyName = (baseModel, joinedModel, schema$1) => {
38
+ try {
39
+ const defaultBaseModelName = getDefaultModelName(baseModel);
40
+ const defaultJoinedModelName = getDefaultModelName(joinedModel);
41
+ const key = getModelName(joinedModel).toLowerCase();
42
+ let foreignKeys = Object.entries(schema$1[defaultJoinedModelName]?.fields || {}).filter(([_field, fieldAttributes]) => fieldAttributes.references && getDefaultModelName(fieldAttributes.references.model) === defaultBaseModelName);
43
+ if (foreignKeys.length > 0) {
44
+ const [_foreignKey, foreignKeyAttributes] = foreignKeys[0];
45
+ return foreignKeyAttributes?.unique === true || config.usePlural === true ? key : `${key}s`;
46
+ }
47
+ foreignKeys = Object.entries(schema$1[defaultBaseModelName]?.fields || {}).filter(([_field, fieldAttributes]) => fieldAttributes.references && getDefaultModelName(fieldAttributes.references.model) === defaultJoinedModelName);
48
+ if (foreignKeys.length > 0) return key;
49
+ } catch {}
50
+ return `${getModelName(joinedModel).toLowerCase()}s`;
51
+ };
52
+ function operatorToPrismaOperator(operator) {
53
+ switch (operator) {
54
+ case "starts_with": return "startsWith";
55
+ case "ends_with": return "endsWith";
56
+ case "ne": return "not";
57
+ case "not_in": return "notIn";
58
+ default: return operator;
59
+ }
60
+ }
61
+ const convertWhereClause = ({ action, model, where }) => {
62
+ if (!where || !where.length) return {};
63
+ const buildSingleCondition = (w) => {
64
+ const fieldName = getFieldName({
65
+ model,
66
+ field: w.field
67
+ });
68
+ if (w.operator === "ne" && w.value === null) return getFieldAttributes({
69
+ model,
70
+ field: w.field
71
+ })?.required !== true ? { [fieldName]: { not: null } } : {};
72
+ if ((w.operator === "in" || w.operator === "not_in") && Array.isArray(w.value)) {
73
+ const filtered = w.value.filter((v) => v != null);
74
+ if (filtered.length === 0) if (w.operator === "in") return { AND: [{ [fieldName]: { equals: "__never__" } }, { [fieldName]: { not: "__never__" } }] };
75
+ else return {};
76
+ const prismaOp = operatorToPrismaOperator(w.operator);
77
+ return { [fieldName]: { [prismaOp]: filtered } };
78
+ }
79
+ if (w.operator === "eq" || !w.operator) return { [fieldName]: w.value };
80
+ return { [fieldName]: { [operatorToPrismaOperator(w.operator)]: w.value } };
81
+ };
82
+ if (action === "update") {
83
+ const and$1 = where.filter((w) => w.connector === "AND" || !w.connector);
84
+ const or$1 = where.filter((w) => w.connector === "OR");
85
+ const andSimple = and$1.filter((w) => w.operator === "eq" || !w.operator);
86
+ const andComplex = and$1.filter((w) => w.operator !== "eq" && w.operator !== void 0);
87
+ const andSimpleClause = andSimple.map((w) => buildSingleCondition(w));
88
+ const andComplexClause = andComplex.map((w) => buildSingleCondition(w));
89
+ const orClause$1 = or$1.map((w) => buildSingleCondition(w));
90
+ const result = {};
91
+ for (const clause of andSimpleClause) Object.assign(result, clause);
92
+ if (andComplexClause.length > 0) result.AND = andComplexClause;
93
+ if (orClause$1.length > 0) result.OR = orClause$1;
94
+ return result;
95
+ }
96
+ if (action === "delete") {
97
+ const idCondition = where.find((w) => w.field === "id");
98
+ if (idCondition) {
99
+ const idFieldName = getFieldName({
100
+ model,
101
+ field: "id"
102
+ });
103
+ const idClause = buildSingleCondition(idCondition);
104
+ const remainingWhere = where.filter((w) => w.field !== "id");
105
+ if (remainingWhere.length === 0) return idClause;
106
+ const and$1 = remainingWhere.filter((w) => w.connector === "AND" || !w.connector);
107
+ const or$1 = remainingWhere.filter((w) => w.connector === "OR");
108
+ const andClause$1 = and$1.map((w) => buildSingleCondition(w));
109
+ const orClause$1 = or$1.map((w) => buildSingleCondition(w));
110
+ const result = {};
111
+ if (idFieldName in idClause) result[idFieldName] = idClause[idFieldName];
112
+ else Object.assign(result, idClause);
113
+ if (andClause$1.length > 0) result.AND = andClause$1;
114
+ if (orClause$1.length > 0) result.OR = orClause$1;
115
+ return result;
116
+ }
117
+ }
118
+ if (where.length === 1) {
119
+ const w = where[0];
120
+ if (!w) return;
121
+ return buildSingleCondition(w);
122
+ }
123
+ const and = where.filter((w) => w.connector === "AND" || !w.connector);
124
+ const or = where.filter((w) => w.connector === "OR");
125
+ const andClause = and.map((w) => buildSingleCondition(w));
126
+ const orClause = or.map((w) => buildSingleCondition(w));
127
+ return {
128
+ ...andClause.length ? { AND: andClause } : {},
129
+ ...orClause.length ? { OR: orClause } : {}
130
+ };
131
+ };
132
+ return {
133
+ async create({ model, data: values, select }) {
134
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
135
+ return await db[model].create({
136
+ data: values,
137
+ select: convertSelect(select, model)
138
+ });
139
+ },
140
+ async findOne({ model, where, select, join }) {
141
+ const whereClause = convertWhereClause({
142
+ model,
143
+ where,
144
+ action: "findOne"
145
+ });
146
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
147
+ const map = /* @__PURE__ */ new Map();
148
+ for (const joinModel of Object.keys(join ?? {})) {
149
+ const key = getJoinKeyName(model, joinModel, schema);
150
+ map.set(key, getModelName(joinModel));
151
+ }
152
+ const selects = convertSelect(select, model, join);
153
+ const result = await db[model].findFirst({
154
+ where: whereClause,
155
+ select: selects
156
+ });
157
+ if (join && result) for (const [includeKey, originalKey] of map.entries()) {
158
+ if (includeKey === originalKey) continue;
159
+ if (includeKey in result) {
160
+ result[originalKey] = result[includeKey];
161
+ delete result[includeKey];
162
+ }
163
+ }
164
+ return result;
165
+ },
166
+ async findMany({ model, where, limit, offset, sortBy, join }) {
167
+ const whereClause = convertWhereClause({
168
+ model,
169
+ where,
170
+ action: "findMany"
171
+ });
172
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
173
+ const map = /* @__PURE__ */ new Map();
174
+ if (join) for (const [joinModel, _value] of Object.entries(join)) {
175
+ const key = getJoinKeyName(model, joinModel, schema);
176
+ map.set(key, getModelName(joinModel));
177
+ }
178
+ const selects = convertSelect(void 0, model, join);
179
+ const result = await db[model].findMany({
180
+ where: whereClause,
181
+ take: limit || 100,
182
+ skip: offset || 0,
183
+ ...sortBy?.field ? { orderBy: { [getFieldName({
184
+ model,
185
+ field: sortBy.field
186
+ })]: sortBy.direction === "desc" ? "desc" : "asc" } } : {},
187
+ select: selects
188
+ });
189
+ if (join && Array.isArray(result)) for (const item of result) for (const [includeKey, originalKey] of map.entries()) {
190
+ if (includeKey === originalKey) continue;
191
+ if (includeKey in item) {
192
+ item[originalKey] = item[includeKey];
193
+ delete item[includeKey];
194
+ }
195
+ }
196
+ return result;
197
+ },
198
+ async count({ model, where }) {
199
+ const whereClause = convertWhereClause({
200
+ model,
201
+ where,
202
+ action: "count"
203
+ });
204
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
205
+ return await db[model].count({ where: whereClause });
206
+ },
207
+ async update({ model, where, update }) {
208
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
209
+ const whereClause = convertWhereClause({
210
+ model,
211
+ where,
212
+ action: "update"
213
+ });
214
+ return await db[model].update({
215
+ where: whereClause,
216
+ data: update
217
+ });
218
+ },
219
+ async updateMany({ model, where, update }) {
220
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
221
+ const whereClause = convertWhereClause({
222
+ model,
223
+ where,
224
+ action: "updateMany"
225
+ });
226
+ const result = await db[model].updateMany({
227
+ where: whereClause,
228
+ data: update
229
+ });
230
+ return result ? result.count : 0;
231
+ },
232
+ async delete({ model, where }) {
233
+ if (!db[model]) throw new BetterAuthError(`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`);
234
+ const whereClause = convertWhereClause({
235
+ model,
236
+ where,
237
+ action: "delete"
238
+ });
239
+ try {
240
+ await db[model].delete({ where: whereClause });
241
+ } catch (e) {
242
+ if (e?.meta?.cause === "Record to delete does not exist.") return;
243
+ if (e?.code === "P2025") return;
244
+ console.log(e);
245
+ }
246
+ },
247
+ async deleteMany({ model, where }) {
248
+ const whereClause = convertWhereClause({
249
+ model,
250
+ where,
251
+ action: "deleteMany"
252
+ });
253
+ const result = await db[model].deleteMany({ where: whereClause });
254
+ return result ? result.count : 0;
255
+ },
256
+ options: config
257
+ };
258
+ };
259
+ let adapterOptions = null;
260
+ adapterOptions = {
261
+ config: {
262
+ adapterId: "prisma",
263
+ adapterName: "Prisma Adapter",
264
+ usePlural: config.usePlural ?? false,
265
+ debugLogs: config.debugLogs ?? false,
266
+ supportsUUIDs: config.provider === "postgresql" ? true : false,
267
+ supportsArrays: config.provider === "postgresql" || config.provider === "mongodb" ? true : false,
268
+ transaction: config.transaction ?? false ? (cb) => prisma.$transaction((tx) => {
269
+ return cb(createAdapterFactory({
270
+ config: adapterOptions.config,
271
+ adapter: createCustomAdapter(tx)
272
+ })(lazyOptions));
273
+ }) : false
274
+ },
275
+ adapter: createCustomAdapter(prisma)
276
+ };
277
+ const adapter = createAdapterFactory(adapterOptions);
278
+ return (options) => {
279
+ lazyOptions = options;
280
+ return adapter(options);
281
+ };
282
+ };
283
+
284
+ //#endregion
285
+ export { prismaAdapter };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@better-auth/prisma-adapter",
3
+ "version": "1.5.0-beta.9",
4
+ "description": "Prisma adapter for Better Auth",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/better-auth/better-auth.git",
9
+ "directory": "packages/prisma-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
+ "dependencies": {
22
+ "@prisma/client": "^5.0.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
26
+ "@better-auth/utils": "^0.3.0",
27
+ "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
28
+ "@better-auth/core": "1.5.0-beta.9"
29
+ },
30
+ "devDependencies": {
31
+ "@better-auth/utils": "^0.3.0",
32
+ "prisma": "^5.0.0",
33
+ "tsdown": "^0.19.0",
34
+ "typescript": "^5.9.3",
35
+ "@better-auth/core": "1.5.0-beta.9"
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "dev": "tsdown --watch",
40
+ "test": "vitest",
41
+ "typecheck": "tsc --noEmit"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./prisma-adapter";
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { prismaAdapter } from "./prisma-adapter";
3
+
4
+ describe("prisma-adapter", () => {
5
+ it("should create prisma adapter", () => {
6
+ const prisma = {
7
+ $transaction: vi.fn(),
8
+ } as any;
9
+ const adapter = prismaAdapter(prisma, {
10
+ provider: "sqlite",
11
+ });
12
+ expect(adapter).toBeDefined();
13
+ });
14
+ });
@@ -0,0 +1,564 @@
1
+ import type { Awaitable, BetterAuthOptions } from "@better-auth/core";
2
+ import type {
3
+ AdapterFactoryCustomizeAdapterCreator,
4
+ AdapterFactoryOptions,
5
+ DBAdapter,
6
+ DBAdapterDebugLogOption,
7
+ JoinConfig,
8
+ Where,
9
+ } from "@better-auth/core/db/adapter";
10
+ import { createAdapterFactory } from "@better-auth/core/db/adapter";
11
+ import { BetterAuthError } from "@better-auth/core/error";
12
+
13
+ export interface PrismaConfig {
14
+ /**
15
+ * Database provider.
16
+ */
17
+ provider:
18
+ | "sqlite"
19
+ | "cockroachdb"
20
+ | "mysql"
21
+ | "postgresql"
22
+ | "sqlserver"
23
+ | "mongodb";
24
+
25
+ /**
26
+ * Enable debug logs for the adapter
27
+ *
28
+ * @default false
29
+ */
30
+ debugLogs?: DBAdapterDebugLogOption | undefined;
31
+
32
+ /**
33
+ * Use plural table names
34
+ *
35
+ * @default false
36
+ */
37
+ usePlural?: boolean | undefined;
38
+
39
+ /**
40
+ * Whether to execute multiple operations in a transaction.
41
+ *
42
+ * If the database doesn't support transactions,
43
+ * set this to `false` and operations will be executed sequentially.
44
+ * @default false
45
+ */
46
+ transaction?: boolean | undefined;
47
+ }
48
+
49
+ interface PrismaClient {}
50
+
51
+ type PrismaClientInternal = {
52
+ $transaction: (
53
+ callback: (db: PrismaClient) => Awaitable<any>,
54
+ ) => Promise<any>;
55
+ } & {
56
+ [model: string]: {
57
+ create: (data: any) => Promise<any>;
58
+ findFirst: (data: any) => Promise<any>;
59
+ findMany: (data: any) => Promise<any>;
60
+ update: (data: any) => Promise<any>;
61
+ updateMany: (data: any) => Promise<any>;
62
+ delete: (data: any) => Promise<any>;
63
+ [key: string]: any;
64
+ };
65
+ };
66
+
67
+ export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) => {
68
+ let lazyOptions: BetterAuthOptions | null = null;
69
+ const createCustomAdapter =
70
+ (prisma: PrismaClient): AdapterFactoryCustomizeAdapterCreator =>
71
+ ({
72
+ getFieldName,
73
+ getModelName,
74
+ getFieldAttributes,
75
+ getDefaultModelName,
76
+ schema,
77
+ }) => {
78
+ const db = prisma as PrismaClientInternal;
79
+
80
+ const convertSelect = (
81
+ select: string[] | undefined,
82
+ model: string,
83
+ join?: JoinConfig | undefined,
84
+ ) => {
85
+ if (!select && !join) return undefined;
86
+
87
+ const result: Record<string, Record<string, any> | boolean> = {};
88
+
89
+ if (select) {
90
+ for (const field of select) {
91
+ result[getFieldName({ model, field })] = true;
92
+ }
93
+ }
94
+
95
+ if (join) {
96
+ // when joining that has a limit, we need to use Prisma's `select` syntax to append the limit to the field
97
+ // because of such, it also means we need to select all base-model fields as well
98
+ // should check if `select` is not provided, because then we should select all base-model fields
99
+ if (!select) {
100
+ const fields = schema[getDefaultModelName(model)]?.fields || {};
101
+ fields.id = { type: "string" }; // make sure there is at least an id field
102
+ for (const field of Object.keys(fields)) {
103
+ result[getFieldName({ model, field })] = true;
104
+ }
105
+ }
106
+
107
+ for (const [joinModel, joinAttr] of Object.entries(join)) {
108
+ const key = getJoinKeyName(model, getModelName(joinModel), schema);
109
+ if (joinAttr.relation === "one-to-one") {
110
+ result[key] = true;
111
+ } else {
112
+ result[key] = { take: joinAttr.limit };
113
+ }
114
+ }
115
+ }
116
+
117
+ return result;
118
+ };
119
+
120
+ /**
121
+ * Build the join key name based on whether the foreign field is unique or not.
122
+ * If unique, use singular. Otherwise, pluralize (add 's').
123
+ */
124
+ const getJoinKeyName = (
125
+ baseModel: string,
126
+ joinedModel: string,
127
+ schema: any,
128
+ ): string => {
129
+ try {
130
+ const defaultBaseModelName = getDefaultModelName(baseModel);
131
+ const defaultJoinedModelName = getDefaultModelName(joinedModel);
132
+ const key = getModelName(joinedModel).toLowerCase();
133
+
134
+ // First, check if the joined model has FKs to the base model (forward join)
135
+ let foreignKeys = Object.entries(
136
+ schema[defaultJoinedModelName]?.fields || {},
137
+ ).filter(
138
+ ([_field, fieldAttributes]: any) =>
139
+ fieldAttributes.references &&
140
+ getDefaultModelName(fieldAttributes.references.model) ===
141
+ defaultBaseModelName,
142
+ );
143
+
144
+ if (foreignKeys.length > 0) {
145
+ // Forward join: joined model has FK to base model
146
+ // This is typically a one-to-many relationship (plural)
147
+ // Unless the FK is unique, then it's one-to-one (singular)
148
+ const [_foreignKey, foreignKeyAttributes] = foreignKeys[0] as any;
149
+ // Only check if field is explicitly marked as unique
150
+ const isUnique = foreignKeyAttributes?.unique === true;
151
+ return isUnique || config.usePlural === true ? key : `${key}s`;
152
+ }
153
+
154
+ // Check backwards: does the base model have FKs to the joined model?
155
+ foreignKeys = Object.entries(
156
+ schema[defaultBaseModelName]?.fields || {},
157
+ ).filter(
158
+ ([_field, fieldAttributes]: any) =>
159
+ fieldAttributes.references &&
160
+ getDefaultModelName(fieldAttributes.references.model) ===
161
+ defaultJoinedModelName,
162
+ );
163
+
164
+ if (foreignKeys.length > 0) {
165
+ return key;
166
+ }
167
+ } catch {
168
+ // Fallback to pluralizing if we can't determine uniqueness
169
+ }
170
+ return `${getModelName(joinedModel).toLowerCase()}s`;
171
+ };
172
+ function operatorToPrismaOperator(operator: string) {
173
+ switch (operator) {
174
+ case "starts_with":
175
+ return "startsWith";
176
+ case "ends_with":
177
+ return "endsWith";
178
+ case "ne":
179
+ return "not";
180
+ case "not_in":
181
+ return "notIn";
182
+ default:
183
+ return operator;
184
+ }
185
+ }
186
+ const convertWhereClause = ({
187
+ action,
188
+ model,
189
+ where,
190
+ }: {
191
+ model: string;
192
+ where?: Where[] | undefined;
193
+ action:
194
+ | "create"
195
+ | "update"
196
+ | "delete"
197
+ | "findOne"
198
+ | "findMany"
199
+ | "count"
200
+ | "updateMany"
201
+ | "deleteMany";
202
+ }) => {
203
+ if (!where || !where.length) return {};
204
+ const buildSingleCondition = (w: Where) => {
205
+ const fieldName = getFieldName({ model, field: w.field });
206
+ // Special handling for Prisma null semantics, for non-nullable fields this is a tautology. Skip condition.
207
+ if (w.operator === "ne" && w.value === null) {
208
+ const fieldAttributes = getFieldAttributes({
209
+ model,
210
+ field: w.field,
211
+ });
212
+ const isNullable = fieldAttributes?.required !== true;
213
+ return isNullable ? { [fieldName]: { not: null } } : {};
214
+ }
215
+ if (
216
+ (w.operator === "in" || w.operator === "not_in") &&
217
+ Array.isArray(w.value)
218
+ ) {
219
+ const filtered = w.value.filter((v) => v != null);
220
+ if (filtered.length === 0) {
221
+ if (w.operator === "in") {
222
+ return {
223
+ AND: [
224
+ { [fieldName]: { equals: "__never__" } },
225
+ { [fieldName]: { not: "__never__" } },
226
+ ],
227
+ };
228
+ } else {
229
+ return {};
230
+ }
231
+ }
232
+ const prismaOp = operatorToPrismaOperator(w.operator);
233
+ return { [fieldName]: { [prismaOp]: filtered } };
234
+ }
235
+ if (w.operator === "eq" || !w.operator) {
236
+ return { [fieldName]: w.value };
237
+ }
238
+ return {
239
+ [fieldName]: {
240
+ [operatorToPrismaOperator(w.operator)]: w.value,
241
+ },
242
+ };
243
+ };
244
+
245
+ // Special handling for update actions: extract AND conditions with eq operator to root level
246
+ // Prisma requires unique fields to be at root level, not nested in AND arrays
247
+ // Only simple equality conditions can be at root level; complex operators must stay in AND array
248
+ if (action === "update") {
249
+ const and = where.filter(
250
+ (w) => w.connector === "AND" || !w.connector,
251
+ );
252
+ const or = where.filter((w) => w.connector === "OR");
253
+
254
+ // Separate AND conditions into simple eq (can extract) and complex (must stay in AND)
255
+ const andSimple = and.filter(
256
+ (w) => w.operator === "eq" || !w.operator,
257
+ );
258
+ const andComplex = and.filter(
259
+ (w) => w.operator !== "eq" && w.operator !== undefined,
260
+ );
261
+
262
+ const andSimpleClause = andSimple.map((w) => buildSingleCondition(w));
263
+ const andComplexClause = andComplex.map((w) =>
264
+ buildSingleCondition(w),
265
+ );
266
+ const orClause = or.map((w) => buildSingleCondition(w));
267
+
268
+ // Extract simple equality AND conditions to root level
269
+ const result: Record<string, any> = {};
270
+ for (const clause of andSimpleClause) {
271
+ Object.assign(result, clause);
272
+ }
273
+ // Keep complex AND conditions in AND array
274
+ if (andComplexClause.length > 0) {
275
+ result.AND = andComplexClause;
276
+ }
277
+ if (orClause.length > 0) {
278
+ result.OR = orClause;
279
+ }
280
+ return result;
281
+ }
282
+
283
+ // Special handling for delete actions: extract id to root level
284
+ if (action === "delete") {
285
+ const idCondition = where.find((w) => w.field === "id");
286
+ if (idCondition) {
287
+ const idFieldName = getFieldName({ model, field: "id" });
288
+ const idClause = buildSingleCondition(idCondition);
289
+ const remainingWhere = where.filter((w) => w.field !== "id");
290
+
291
+ if (remainingWhere.length === 0) {
292
+ return idClause;
293
+ }
294
+
295
+ const and = remainingWhere.filter(
296
+ (w) => w.connector === "AND" || !w.connector,
297
+ );
298
+ const or = remainingWhere.filter((w) => w.connector === "OR");
299
+ const andClause = and.map((w) => buildSingleCondition(w));
300
+ const orClause = or.map((w) => buildSingleCondition(w));
301
+
302
+ // Extract id to root level, put other conditions in AND array
303
+ const result: Record<string, any> = {};
304
+ if (idFieldName in idClause) {
305
+ result[idFieldName] = (idClause as Record<string, any>)[
306
+ idFieldName
307
+ ];
308
+ } else {
309
+ // Handle edge case where idClause might have special structure
310
+ Object.assign(result, idClause);
311
+ }
312
+ if (andClause.length > 0) {
313
+ result.AND = andClause;
314
+ }
315
+ if (orClause.length > 0) {
316
+ result.OR = orClause;
317
+ }
318
+ return result;
319
+ }
320
+ }
321
+
322
+ if (where.length === 1) {
323
+ const w = where[0]!;
324
+ if (!w) {
325
+ return;
326
+ }
327
+ return buildSingleCondition(w);
328
+ }
329
+ const and = where.filter((w) => w.connector === "AND" || !w.connector);
330
+ const or = where.filter((w) => w.connector === "OR");
331
+ const andClause = and.map((w) => buildSingleCondition(w));
332
+ const orClause = or.map((w) => buildSingleCondition(w));
333
+
334
+ return {
335
+ ...(andClause.length ? { AND: andClause } : {}),
336
+ ...(orClause.length ? { OR: orClause } : {}),
337
+ };
338
+ };
339
+
340
+ return {
341
+ async create({ model, data: values, select }) {
342
+ if (!db[model]) {
343
+ throw new BetterAuthError(
344
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
345
+ );
346
+ }
347
+ const result = await db[model]!.create({
348
+ data: values,
349
+ select: convertSelect(select, model),
350
+ });
351
+ return result;
352
+ },
353
+ async findOne({ model, where, select, join }) {
354
+ // this is just "JoinOption" type because we disabled join transformation in adapter config
355
+ const whereClause = convertWhereClause({
356
+ model,
357
+ where,
358
+ action: "findOne",
359
+ });
360
+ if (!db[model]) {
361
+ throw new BetterAuthError(
362
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
363
+ );
364
+ }
365
+
366
+ // transform join keys to use Prisma expected field names
367
+ const map = new Map<string, string>();
368
+ for (const joinModel of Object.keys(join ?? {})) {
369
+ const key = getJoinKeyName(model, joinModel, schema);
370
+ map.set(key, getModelName(joinModel));
371
+ }
372
+
373
+ const selects = convertSelect(select, model, join);
374
+
375
+ const result = await db[model]!.findFirst({
376
+ where: whereClause,
377
+ select: selects,
378
+ });
379
+
380
+ // transform the resulting `include` items to use better-auth expected field names
381
+ if (join && result) {
382
+ for (const [includeKey, originalKey] of map.entries()) {
383
+ if (includeKey === originalKey) continue;
384
+ if (includeKey in result) {
385
+ result[originalKey] = result[includeKey];
386
+ delete result[includeKey];
387
+ }
388
+ }
389
+ }
390
+ return result;
391
+ },
392
+ async findMany({ model, where, limit, offset, sortBy, join }) {
393
+ // this is just "JoinOption" type because we disabled join transformation in adapter config
394
+ const whereClause = convertWhereClause({
395
+ model,
396
+ where,
397
+ action: "findMany",
398
+ });
399
+ if (!db[model]) {
400
+ throw new BetterAuthError(
401
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
402
+ );
403
+ }
404
+ // transform join keys to use Prisma expected field names
405
+ const map = new Map<string, string>();
406
+ if (join) {
407
+ for (const [joinModel, _value] of Object.entries(join)) {
408
+ const key = getJoinKeyName(model, joinModel, schema);
409
+ map.set(key, getModelName(joinModel));
410
+ }
411
+ }
412
+
413
+ const selects = convertSelect(undefined, model, join);
414
+
415
+ const result = await db[model]!.findMany({
416
+ where: whereClause,
417
+ take: limit || 100,
418
+ skip: offset || 0,
419
+ ...(sortBy?.field
420
+ ? {
421
+ orderBy: {
422
+ [getFieldName({ model, field: sortBy.field })]:
423
+ sortBy.direction === "desc" ? "desc" : "asc",
424
+ },
425
+ }
426
+ : {}),
427
+ select: selects,
428
+ });
429
+
430
+ // transform the resulting join items to use better-auth expected field names
431
+ if (join && Array.isArray(result)) {
432
+ for (const item of result) {
433
+ for (const [includeKey, originalKey] of map.entries()) {
434
+ if (includeKey === originalKey) continue;
435
+ if (includeKey in item) {
436
+ item[originalKey] = item[includeKey];
437
+ delete item[includeKey];
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ return result;
444
+ },
445
+ async count({ model, where }) {
446
+ const whereClause = convertWhereClause({
447
+ model,
448
+ where,
449
+ action: "count",
450
+ });
451
+ if (!db[model]) {
452
+ throw new BetterAuthError(
453
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
454
+ );
455
+ }
456
+ return await db[model]!.count({
457
+ where: whereClause,
458
+ });
459
+ },
460
+ async update({ model, where, update }) {
461
+ if (!db[model]) {
462
+ throw new BetterAuthError(
463
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
464
+ );
465
+ }
466
+ const whereClause = convertWhereClause({
467
+ model,
468
+ where,
469
+ action: "update",
470
+ });
471
+
472
+ return await db[model]!.update({
473
+ where: whereClause,
474
+ data: update,
475
+ });
476
+ },
477
+ async updateMany({ model, where, update }) {
478
+ if (!db[model]) {
479
+ throw new BetterAuthError(
480
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
481
+ );
482
+ }
483
+ const whereClause = convertWhereClause({
484
+ model,
485
+ where,
486
+ action: "updateMany",
487
+ });
488
+ const result = await db[model]!.updateMany({
489
+ where: whereClause,
490
+ data: update,
491
+ });
492
+ return result ? (result.count as number) : 0;
493
+ },
494
+ async delete({ model, where }) {
495
+ if (!db[model]) {
496
+ throw new BetterAuthError(
497
+ `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
498
+ );
499
+ }
500
+ const whereClause = convertWhereClause({
501
+ model,
502
+ where,
503
+ action: "delete",
504
+ });
505
+ try {
506
+ await db[model]!.delete({
507
+ where: whereClause,
508
+ });
509
+ } catch (e: any) {
510
+ // If the record doesn't exist, we don't want to throw an error
511
+ if (e?.meta?.cause === "Record to delete does not exist.") return;
512
+ if (e?.code === "P2025") return; // Prisma 7+
513
+ // otherwise if it's an unknown error, we want to just log it for debugging.
514
+ console.log(e);
515
+ }
516
+ },
517
+ async deleteMany({ model, where }) {
518
+ const whereClause = convertWhereClause({
519
+ model,
520
+ where,
521
+ action: "deleteMany",
522
+ });
523
+ const result = await db[model]!.deleteMany({
524
+ where: whereClause,
525
+ });
526
+ return result ? (result.count as number) : 0;
527
+ },
528
+ options: config,
529
+ };
530
+ };
531
+
532
+ let adapterOptions: AdapterFactoryOptions | null = null;
533
+ adapterOptions = {
534
+ config: {
535
+ adapterId: "prisma",
536
+ adapterName: "Prisma Adapter",
537
+ usePlural: config.usePlural ?? false,
538
+ debugLogs: config.debugLogs ?? false,
539
+ supportsUUIDs: config.provider === "postgresql" ? true : false,
540
+ supportsArrays:
541
+ config.provider === "postgresql" || config.provider === "mongodb"
542
+ ? true
543
+ : false,
544
+ transaction:
545
+ (config.transaction ?? false)
546
+ ? (cb) =>
547
+ (prisma as PrismaClientInternal).$transaction((tx) => {
548
+ const adapter = createAdapterFactory({
549
+ config: adapterOptions!.config,
550
+ adapter: createCustomAdapter(tx),
551
+ })(lazyOptions!);
552
+ return cb(adapter);
553
+ })
554
+ : false,
555
+ },
556
+ adapter: createCustomAdapter(prisma),
557
+ };
558
+
559
+ const adapter = createAdapterFactory(adapterOptions);
560
+ return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
561
+ lazyOptions = options;
562
+ return adapter(options);
563
+ };
564
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["./src"],
4
+ "references": [
5
+ {
6
+ "path": "../core/tsconfig.json"
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ dts: { build: true, incremental: true },
5
+ format: ["esm"],
6
+ entry: ["./src/index.ts"],
7
+ });
@@ -0,0 +1,3 @@
1
+ import { defineProject } from "vitest/config";
2
+
3
+ export default defineProject({});