diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts index 73b220fa..f6cae441 100644 --- a/server/auth/sessions/app.ts +++ b/server/auth/sessions/app.ts @@ -3,7 +3,14 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { resourceSessions, Session, sessions, User, users } from "@server/db"; +import { + resourceSessions, + safeRead, + Session, + sessions, + User, + users +} from "@server/db"; import { db } from "@server/db"; import { eq, inArray } from "drizzle-orm"; import config from "@server/lib/config"; @@ -54,11 +61,15 @@ export async function validateSessionToken( const sessionId = encodeHexLowerCase( sha256(new TextEncoder().encode(token)) ); - const result = await db - .select({ user: users, session: sessions }) - .from(sessions) - .innerJoin(users, eq(sessions.userId, users.userId)) - .where(eq(sessions.sessionId, sessionId)); + + const result = await safeRead((db) => + db + .select({ user: users, session: sessions }) + .from(sessions) + .innerJoin(users, eq(sessions.userId, users.userId)) + .where(eq(sessions.sessionId, sessionId)) + ); + if (result.length < 1) { return { session: null, user: null }; } diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 9a5b2b5f..3b9da3d7 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -1,7 +1,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; import { resourceSessions, ResourceSession } from "@server/db"; -import { db } from "@server/db"; +import { db, safeRead } from "@server/db"; import { eq, and } from "drizzle-orm"; import config from "@server/lib/config"; @@ -66,15 +66,17 @@ export async function validateResourceSessionToken( const sessionId = encodeHexLowerCase( sha256(new TextEncoder().encode(token)) ); - const result = await db - .select() - .from(resourceSessions) - .where( - and( - eq(resourceSessions.sessionId, sessionId), - eq(resourceSessions.resourceId, resourceId) + const result = await safeRead((db) => + db + .select() + .from(resourceSessions) + .where( + and( + eq(resourceSessions.sessionId, sessionId), + eq(resourceSessions.resourceId, resourceId) + ) ) - ); + ); if (result.length < 1) { return { resourceSession: null }; diff --git a/server/db/pg/index.ts b/server/db/pg/index.ts index 43e2650f..86e31802 100644 --- a/server/db/pg/index.ts +++ b/server/db/pg/index.ts @@ -1,4 +1,5 @@ export * from "./driver"; +export * from "./safeRead"; export * from "./schema/schema"; export * from "./schema/privateSchema"; export * from "./migrate"; diff --git a/server/db/pg/safeRead.ts b/server/db/pg/safeRead.ts new file mode 100644 index 00000000..eac9ac31 --- /dev/null +++ b/server/db/pg/safeRead.ts @@ -0,0 +1,24 @@ +import { db, primaryDb } from "./driver"; + +/** + * Runs a read query with replica fallback for Postgres. + * Executes the query against the replica first (when replicas exist). + * If the query throws or returns no data (null, undefined, or empty array), + * runs the same query against the primary. + */ +export async function safeRead( + query: (d: typeof db | typeof primaryDb) => Promise +): Promise { + try { + const result = await query(db); + if (result === undefined || result === null) { + return query(primaryDb); + } + if (Array.isArray(result) && result.length === 0) { + return query(primaryDb); + } + return result; + } catch { + return query(primaryDb); + } +} diff --git a/server/db/sqlite/index.ts b/server/db/sqlite/index.ts index 43e2650f..86e31802 100644 --- a/server/db/sqlite/index.ts +++ b/server/db/sqlite/index.ts @@ -1,4 +1,5 @@ export * from "./driver"; +export * from "./safeRead"; export * from "./schema/schema"; export * from "./schema/privateSchema"; export * from "./migrate"; diff --git a/server/db/sqlite/safeRead.ts b/server/db/sqlite/safeRead.ts new file mode 100644 index 00000000..6d3e9068 --- /dev/null +++ b/server/db/sqlite/safeRead.ts @@ -0,0 +1,11 @@ +import { db } from "./driver"; + +/** + * Runs a read query. For SQLite there is no replica/primary distinction, + * so the query is executed once against the database. + */ +export async function safeRead( + query: (d: typeof db) => Promise +): Promise { + return query(db); +}