Skip to content

Commit 3f16e9f

Browse files
committed
fix(prisma-adapter): fall back to updateMany for non-unique updates (#8524)
1 parent d803657 commit 3f16e9f

File tree

2 files changed

+152
-2
lines changed

2 files changed

+152
-2
lines changed
Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,113 @@
1+
import type { BetterAuthOptions } from "@better-auth/core";
12
import { describe, expect, it, vi } from "vitest";
23
import { prismaAdapter } from "./prisma-adapter";
34

45
describe("prisma-adapter", () => {
6+
const createTestAdapter = (prisma: Record<string, unknown>) =>
7+
prismaAdapter(prisma as never, {
8+
provider: "sqlite",
9+
})({} as BetterAuthOptions);
10+
511
it("should create prisma adapter", () => {
612
const prisma = {
713
$transaction: vi.fn(),
8-
} as any;
9-
const adapter = prismaAdapter(prisma, {
14+
};
15+
const adapter = prismaAdapter(prisma as never, {
1016
provider: "sqlite",
1117
});
1218
expect(adapter).toBeDefined();
1319
});
20+
21+
/**
22+
* @see https://github.com/better-auth/better-auth/issues/8365
23+
*/
24+
it("should fall back to updateMany for non-unique verification identifiers", async () => {
25+
const update = vi.fn();
26+
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
27+
const findFirst = vi.fn().mockResolvedValue({
28+
id: "verification-id",
29+
identifier: "magic-link-token",
30+
value: "updated-value",
31+
});
32+
const adapter = createTestAdapter({
33+
$transaction: vi.fn(),
34+
verification: {
35+
findFirst,
36+
update,
37+
updateMany,
38+
},
39+
});
40+
41+
const result = await adapter.update({
42+
model: "verification",
43+
where: [{ field: "identifier", value: "magic-link-token" }],
44+
update: { value: "updated-value" },
45+
});
46+
47+
expect(update).not.toHaveBeenCalled();
48+
expect(updateMany).toHaveBeenCalledWith(
49+
expect.objectContaining({
50+
where: {
51+
identifier: "magic-link-token",
52+
},
53+
data: expect.objectContaining({
54+
value: "updated-value",
55+
updatedAt: expect.any(Date),
56+
}),
57+
}),
58+
);
59+
expect(findFirst).toHaveBeenCalledWith({
60+
where: {
61+
identifier: "magic-link-token",
62+
},
63+
});
64+
expect(result).toEqual({
65+
id: "verification-id",
66+
identifier: "magic-link-token",
67+
value: "updated-value",
68+
});
69+
});
70+
71+
it("should keep using update for unique non-id fields", async () => {
72+
const update = vi.fn().mockResolvedValue({
73+
id: "session-id",
74+
token: "session-token",
75+
userId: "user-id",
76+
});
77+
const updateMany = vi.fn();
78+
const findFirst = vi.fn();
79+
const adapter = createTestAdapter({
80+
$transaction: vi.fn(),
81+
session: {
82+
findFirst,
83+
update,
84+
updateMany,
85+
},
86+
});
87+
88+
const result = await adapter.update({
89+
model: "session",
90+
where: [{ field: "token", value: "session-token" }],
91+
update: { userId: "user-id" },
92+
});
93+
94+
expect(update).toHaveBeenCalledWith(
95+
expect.objectContaining({
96+
where: {
97+
token: "session-token",
98+
},
99+
data: expect.objectContaining({
100+
userId: "user-id",
101+
updatedAt: expect.any(Date),
102+
}),
103+
}),
104+
);
105+
expect(updateMany).not.toHaveBeenCalled();
106+
expect(findFirst).not.toHaveBeenCalled();
107+
expect(result).toEqual({
108+
id: "session-id",
109+
token: "session-token",
110+
userId: "user-id",
111+
});
112+
});
14113
});

packages/prisma-adapter/src/prisma-adapter.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,35 @@ export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) => {
183183
return operator;
184184
}
185185
}
186+
const hasRootUniqueWhereCondition = (
187+
model: string,
188+
where?: Where[] | undefined,
189+
) => {
190+
if (!where?.length) {
191+
return false;
192+
}
193+
194+
return where.some((condition) => {
195+
if (condition.connector === "OR") {
196+
return false;
197+
}
198+
199+
if (condition.operator && condition.operator !== "eq") {
200+
return false;
201+
}
202+
203+
if (condition.field === "id") {
204+
return true;
205+
}
206+
207+
return (
208+
getFieldAttributes({
209+
model,
210+
field: condition.field,
211+
})?.unique === true
212+
);
213+
});
214+
};
186215
const convertWhereClause = ({
187216
action,
188217
model,
@@ -463,6 +492,28 @@ export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) => {
463492
`Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`,
464493
);
465494
}
495+
const hasRootUniqueCondition = hasRootUniqueWhereCondition(
496+
model,
497+
where,
498+
);
499+
if (!hasRootUniqueCondition) {
500+
const whereClause = convertWhereClause({
501+
model,
502+
where,
503+
action: "updateMany",
504+
});
505+
const result = await db[model]!.updateMany({
506+
where: whereClause,
507+
data: update,
508+
});
509+
if (!result?.count) {
510+
return null;
511+
}
512+
513+
return await db[model]!.findFirst({
514+
where: whereClause,
515+
});
516+
}
466517
const whereClause = convertWhereClause({
467518
model,
468519
where,

0 commit comments

Comments
 (0)