refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (5)
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
9aa4f331bf
commit
30b54e99e7
40 files changed
+1183
-784
No files matched your search
@@ -1,14 +1,16 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockFindUnique = vi.hoisted(() => vi.fn());
|
||||
const mockLimit = vi.hoisted(() => vi.fn());
|
||||
const mockWhere = vi.hoisted(() => vi.fn(() => ({ limit: mockLimit })));
|
||||
const mockFrom = vi.hoisted(() => vi.fn(() => ({ where: mockWhere })));
|
||||
const mockSelect = vi.hoisted(() => vi.fn(() => ({ from: mockFrom })));
|
||||
const mockRedisGet = vi.hoisted(() => vi.fn());
|
||||
const mockRedisSetex = vi.hoisted(() => vi.fn());
|
||||
const mockRedisDel = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/lib/prisma", () => ({
|
||||
prisma: {
|
||||
user: { findUnique: mockFindUnique },
|
||||
},
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: { select: mockSelect },
|
||||
User: { id: "id", websiteJwtVersion: "websiteJwtVersion" },
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/redis", () => ({
|
||||
@@ -23,29 +25,32 @@ describe("jwt-version-cache", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
mockWhere.mockReturnValue({ limit: mockLimit });
|
||||
mockFrom.mockReturnValue({ where: mockWhere });
|
||||
mockSelect.mockReturnValue({ from: mockFrom });
|
||||
mockRedisGet.mockResolvedValue(null);
|
||||
mockRedisSetex.mockResolvedValue("OK");
|
||||
mockRedisDel.mockResolvedValue(1);
|
||||
});
|
||||
|
||||
it("returns DB version and caches it", async () => {
|
||||
mockFindUnique.mockResolvedValue({ websiteJwtVersion: 3 });
|
||||
mockLimit.mockResolvedValue([{ websiteJwtVersion: 3 }]);
|
||||
const { getCachedJwtVersion } = await import("./jwt-version-cache");
|
||||
await expect(getCachedJwtVersion(42)).resolves.toBe(3);
|
||||
expect(mockFindUnique).toHaveBeenCalledTimes(1);
|
||||
expect(mockSelect).toHaveBeenCalledTimes(1);
|
||||
await expect(getCachedJwtVersion(42)).resolves.toBe(3);
|
||||
expect(mockFindUnique).toHaveBeenCalledTimes(1);
|
||||
expect(mockSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("invalidates memory and redis entries", async () => {
|
||||
mockFindUnique.mockResolvedValue({ websiteJwtVersion: 1 });
|
||||
mockLimit.mockResolvedValue([{ websiteJwtVersion: 1 }]);
|
||||
const { getCachedJwtVersion, invalidateJwtVersionCache } = await import(
|
||||
"./jwt-version-cache"
|
||||
);
|
||||
await getCachedJwtVersion(7);
|
||||
await invalidateJwtVersionCache(7);
|
||||
expect(mockRedisDel).toHaveBeenCalled();
|
||||
mockFindUnique.mockResolvedValue({ websiteJwtVersion: 2 });
|
||||
mockLimit.mockResolvedValue([{ websiteJwtVersion: 2 }]);
|
||||
await expect(getCachedJwtVersion(7)).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { redis } from "@/lib/redis";
|
||||
|
||||
const MEMORY_TTL_MS = 60_000;
|
||||
@@ -40,10 +41,11 @@ export async function getCachedJwtVersion(
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { websiteJwtVersion: true },
|
||||
});
|
||||
const [row] = await db
|
||||
.select({ websiteJwtVersion: User.websiteJwtVersion })
|
||||
.from(User)
|
||||
.where(eq(User.id, userId))
|
||||
.limit(1);
|
||||
if (!row) return null;
|
||||
const version = row.websiteJwtVersion;
|
||||
memory.set(userId, { version, expiresAt: now + MEMORY_TTL_MS });
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { generateSsoTicket, issueSsoTicket } from "./sso-ticket";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { generateSsoTicket } from "./sso-ticket";
|
||||
|
||||
const UUID_RE =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
const mockWhere = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
const mockSet = vi.hoisted(() => vi.fn(() => ({ where: mockWhere })));
|
||||
const mockUpdate = vi.hoisted(() => vi.fn(() => ({ set: mockSet })));
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: { update: mockUpdate },
|
||||
User: { id: "id" },
|
||||
}));
|
||||
|
||||
describe("generateSsoTicket", () => {
|
||||
it("uses '{hotelName-without-spaces}-{uuidv4}'", () => {
|
||||
const t = generateSsoTicket("Atom Hotel");
|
||||
@@ -23,15 +32,23 @@ describe("generateSsoTicket", () => {
|
||||
});
|
||||
|
||||
describe("issueSsoTicket", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSet.mockReturnValue({ where: mockWhere });
|
||||
mockUpdate.mockReturnValue({ set: mockSet });
|
||||
mockWhere.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("writes auth_ticket AND ip_current and returns the ticket", async () => {
|
||||
const update = vi.fn().mockResolvedValue(undefined);
|
||||
const db = { user: { update } };
|
||||
const ticket = await issueSsoTicket(db, 42, "Atom Hotel", "1.2.3.4");
|
||||
const { issueSsoTicket } = await import("./sso-ticket");
|
||||
const ticket = await issueSsoTicket(42, "Atom Hotel", "1.2.3.4");
|
||||
|
||||
expect(ticket.startsWith("AtomHotel-")).toBe(true);
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: 42 },
|
||||
data: { authTicket: ticket, ipCurrent: "1.2.3.4" },
|
||||
expect(mockUpdate).toHaveBeenCalled();
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
authTicket: ticket,
|
||||
ipCurrent: "1.2.3.4",
|
||||
});
|
||||
expect(mockWhere).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Build the SSO ticket exactly like AtomCMS's User::ssoTicket():
|
||||
@@ -12,30 +14,19 @@ export function generateSsoTicket(hotelName: string): string {
|
||||
return `${normalized}-${randomUUID()}`;
|
||||
}
|
||||
|
||||
/** Minimal shape of the Prisma client this needs (keeps it unit-testable). */
|
||||
export interface SsoUserUpdater {
|
||||
user: {
|
||||
update(args: {
|
||||
where: { id: number };
|
||||
data: { authTicket: string; ipCurrent: string };
|
||||
}): Promise<unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a ticket and persist it like AtomCMS: writes auth_ticket AND
|
||||
* ip_current on the user, then returns the ticket for the client launcher.
|
||||
*/
|
||||
export async function issueSsoTicket(
|
||||
db: SsoUserUpdater,
|
||||
userId: number,
|
||||
hotelName: string,
|
||||
ip: string,
|
||||
): Promise<string> {
|
||||
const ticket = generateSsoTicket(hotelName);
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { authTicket: ticket, ipCurrent: ip },
|
||||
});
|
||||
await db
|
||||
.update(User)
|
||||
.set({ authTicket: ticket, ipCurrent: ip })
|
||||
.where(eq(User.id, userId));
|
||||
return ticket;
|
||||
}
|
||||
Reference in new issue
Block a user