diff --git a/app/controllers/site.controller.ts b/app/controllers/site.controller.ts index a8cf39e6..c026d9af 100644 --- a/app/controllers/site.controller.ts +++ b/app/controllers/site.controller.ts @@ -1,4 +1,4 @@ -import { prisma } from "~/lib/db.server" +import { prismaRead, prismaWrite } from "~/lib/db.server" import { checkPageSlug, checkSubdomain, @@ -7,7 +7,7 @@ import { } from "~/models/site.model" import { PageVisibilityEnum } from "~/lib/types" import { isUUID } from "~/lib/uuid" -import { MembershipRole, PageType, Prisma } from "@prisma/client" +import { MembershipRole, PageType, type Prisma } from "@prisma/client" import { nanoid } from "nanoid" import { z } from "zod" import { type AuthUser } from "~/lib/auth.server" @@ -84,7 +84,7 @@ export const siteController = { } const [pages, total] = await Promise.all([ - prisma.page.findMany({ + prismaRead.page.findMany({ where, orderBy: { createdAt: "desc", @@ -96,7 +96,7 @@ export const siteController = { } : undefined, }), - await prisma.page.count({ + await prismaRead.page.count({ where: { siteId: site.id, }, @@ -129,7 +129,7 @@ export const siteController = { const gate = createGate({ user }) const page = input.pageId - ? await prisma.page.findUnique({ + ? await prismaRead.page.findUnique({ where: { id: input.pageId, }, @@ -137,7 +137,7 @@ export const siteController = { site: true, }, }) - : await prisma.page.create({ + : await prismaWrite.page.create({ data: { title: "Untitled", slug: `untitled-${nanoid(4)}`, @@ -165,7 +165,7 @@ export const siteController = { const slug = input.slug || page.slug await checkPageSlug({ slug, excludePage: page.id, siteId: page.siteId }) - const updated = await prisma.page.update({ + const updated = await prismaWrite.page.update({ where: { id: page.id, }, @@ -204,13 +204,13 @@ export const siteController = { } const page = isPageUUID - ? await prisma.page.findUnique({ + ? await prismaRead.page.findUnique({ where: { id: input.page, }, }) : site - ? await prisma.page.findFirst({ + ? await prismaRead.page.findFirst({ where: { siteId: site.id, slug: input.page }, }) : null @@ -252,7 +252,7 @@ export const siteController = { }) } - const updated = await prisma.site.update({ + const updated = await prismaWrite.site.update({ where: { id: site.id, }, @@ -278,7 +278,7 @@ export const siteController = { await checkSubdomain({ subdomain: payload.subdomain }) - const site = await prisma.site.create({ + const site = await prismaWrite.site.create({ data: { name: payload.name, subdomain: payload.subdomain, diff --git a/app/lib/auth.server.ts b/app/lib/auth.server.ts index 827e96c0..f7234458 100644 --- a/app/lib/auth.server.ts +++ b/app/lib/auth.server.ts @@ -1,6 +1,6 @@ import type { User, Membership } from "@prisma/client" import { AUTH_COOKIE_NAME } from "./config.server" -import { prisma } from "./db.server" +import { prismaRead } from "./db.server" import dayjs from "dayjs" import { type CookieSerializeOptions, createCookie } from "@remix-run/node" import { IS_PROD, OUR_DOMAIN } from "./config.shared" @@ -23,7 +23,7 @@ export const getAuthTokenFromRequest = async (request: Request) => { } const findUserFromToken = async (token: string) => { - const accessToken = await prisma.accessToken.findUnique({ + const accessToken = await prismaRead.accessToken.findUnique({ where: { token, }, diff --git a/app/lib/config.server.ts b/app/lib/config.server.ts index ab1ff2f2..3283972c 100644 --- a/app/lib/config.server.ts +++ b/app/lib/config.server.ts @@ -8,3 +8,7 @@ export const S3_ENDPOINT = process.env.S3_ENDPOINT export const MAILGUN_APIKEY = process.env.MAILGUN_APIKEY export const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN export const MAILGUN_EU = process.env.MAILGUN_EU +// Primary Fly region +export const PRIMARY_REGION = process.env.PRIMARY_REGION +// Curreny Fly region +export const FLY_REGION = process.env.FLY_REGION diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts index 7380b536..923815ac 100644 --- a/app/lib/db.server.ts +++ b/app/lib/db.server.ts @@ -1,7 +1,26 @@ import { PrismaClient } from "@prisma/client" +import { FLY_REGION, PRIMARY_REGION } from "./config.server" +import { IS_PROD } from "./config.shared" import { singleton } from "./singleton.server" -export const prisma = /* @__PURE__ */ singleton( - "prisma", +export const prismaWrite = /* @__PURE__ */ singleton( + "prisma-write", () => new PrismaClient() ) + +// Connect to the read replica of our database on Fly +export const prismaRead = /* @__PURE__ */ singleton("prisma-read", () => { + // 5433 is the read-replica port + let url = process.env.DATABASE_URL + if (FLY_REGION !== PRIMARY_REGION && IS_PROD) { + url = url.replace(":5432", ":5433") + } + console.log("read replica url", url) + return new PrismaClient({ + datasources: { + db: { + url, + }, + }, + }) +}) diff --git a/app/models/site.model.ts b/app/models/site.model.ts index 2d2a294c..f8fdec39 100644 --- a/app/models/site.model.ts +++ b/app/models/site.model.ts @@ -1,4 +1,4 @@ -import { prisma } from "~/lib/db.server" +import { prismaRead, prismaWrite } from "~/lib/db.server" import { isUUID } from "~/lib/uuid" import { MembershipRole } from "@prisma/client" @@ -9,7 +9,7 @@ export const checkSubdomain = async ({ subdomain: string updatingSiteId?: string }) => { - const existingSite = await prisma.site.findUnique({ + const existingSite = await prismaRead.site.findUnique({ where: { subdomain, }, @@ -17,7 +17,7 @@ export const checkSubdomain = async ({ if (existingSite?.deletedAt) { // Actuall delete the site so that the subdomain can be used again - await prisma.site.delete({ + await prismaWrite.site.delete({ where: { id: existingSite.id, }, @@ -31,7 +31,7 @@ export const checkSubdomain = async ({ } export const getUserLastActiveSite = async (userId: string) => { - const memberships = await prisma.membership.findMany({ + const memberships = await prismaRead.membership.findMany({ where: { userId, role: { @@ -55,12 +55,12 @@ export const getUserLastActiveSite = async (userId: string) => { export const getSite = async (input: string) => { const site = isUUID(input) - ? await prisma.site.findUnique({ + ? await prismaRead.site.findUnique({ where: { id: input, }, }) - : await prisma.site.findUnique({ + : await prismaRead.site.findUnique({ where: { subdomain: input, }, @@ -78,7 +78,7 @@ export const getMembership = async (data: { userId: string role: MembershipRole }) => { - const first = await prisma.membership.findFirst({ + const first = await prismaRead.membership.findFirst({ where: { role: data.role, userId: data.userId, @@ -90,7 +90,7 @@ export const getMembership = async (data: { } export const getSitesByUser = async ({ userId }: { userId: string }) => { - const memberships = await prisma.membership.findMany({ + const memberships = await prismaRead.membership.findMany({ where: { userId, role: { @@ -116,7 +116,7 @@ export const checkPageSlug = async ({ if (!slug) { throw new Error("Missing page slug") } - const page = await prisma.page.findFirst({ + const page = await prismaRead.page.findFirst({ where: { siteId, slug, @@ -128,7 +128,7 @@ export const checkPageSlug = async ({ if (!page) return if (page.deletedAt) { - await prisma.page.delete({ + await prismaWrite.page.delete({ where: { id: page.id, }, diff --git a/app/models/user.model.ts b/app/models/user.model.ts index 96e5e6bd..305806d0 100644 --- a/app/models/user.model.ts +++ b/app/models/user.model.ts @@ -1,5 +1,5 @@ import { type AuthUser } from "~/lib/auth.server" -import { prisma } from "~/lib/db.server" +import { prismaRead, prismaWrite } from "~/lib/db.server" import { createGate } from "~/lib/gate.server" export const userModel = { @@ -15,7 +15,7 @@ export const userModel = { ) { const gate = createGate({ user, requireAuth: true }) if (payload.email) { - const userByEmail = await prisma.user.findUnique({ + const userByEmail = await prismaRead.user.findUnique({ where: { email: payload.email, }, @@ -26,7 +26,7 @@ export const userModel = { } if (payload.username) { - const userByUsername = await prisma.user.findUnique({ + const userByUsername = await prismaRead.user.findUnique({ where: { username: payload.username, }, @@ -36,7 +36,7 @@ export const userModel = { } } - const updated = await prisma.user.update({ + const updated = await prismaWrite.user.update({ where: { id: gate.user.id, }, diff --git a/app/routes/api/login-complete.ts b/app/routes/api/login-complete.ts index f653bd63..47de4bf9 100644 --- a/app/routes/api/login-complete.ts +++ b/app/routes/api/login-complete.ts @@ -1,8 +1,8 @@ -import { LoaderFunction, redirect } from "@remix-run/node" +import { type LoaderFunction, redirect } from "@remix-run/node" import { z } from "zod" import { generateCookie } from "~/lib/auth.server" import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared" -import { prisma } from "~/lib/db.server" +import { prismaRead, prismaWrite } from "~/lib/db.server" export const loader: LoaderFunction = async ({ request }) => { const url = new URL(request.url) @@ -22,7 +22,7 @@ export const loader: LoaderFunction = async ({ request }) => { // Set cookie again for custom domain and subdomain.localhost (because *.localhost in cookie domain doesn't work) if (isCustomDomain || !IS_PROD) { - const accessToken = await prisma.accessToken.findUnique({ + const accessToken = await prismaRead.accessToken.findUnique({ where: { publicId: data.id, }, @@ -36,7 +36,7 @@ export const loader: LoaderFunction = async ({ request }) => { throw new Error("invalid id or id expired") } - await prisma.accessToken.update({ + await prismaWrite.accessToken.update({ where: { id: accessToken.id, }, diff --git a/app/routes/api/login.ts b/app/routes/api/login.ts index d92ef4ed..0a9f962e 100644 --- a/app/routes/api/login.ts +++ b/app/routes/api/login.ts @@ -1,6 +1,6 @@ -import { LoaderFunction, redirect } from "@remix-run/node" +import { type LoaderFunction, redirect } from "@remix-run/node" import { z } from "zod" -import { prisma } from "~/lib/db.server" +import { prismaRead, prismaWrite } from "~/lib/db.server" import { nanoid } from "nanoid" import UAParser from "ua-parser-js" import dayjs from "dayjs" @@ -21,7 +21,7 @@ export const loader: LoaderFunction = async ({ request }) => { userAgent: request.headers.get("user-agent"), }) - const loginToken = await prisma.loginToken.findUnique({ + const loginToken = await prismaRead.loginToken.findUnique({ where: { id: data.token, }, @@ -35,14 +35,14 @@ export const loader: LoaderFunction = async ({ request }) => { throw new Error(`token expired`) } - let user = await prisma.user.findUnique({ + let user = await prismaRead.user.findUnique({ where: { email: loginToken.email, }, }) if (!user) { - user = await prisma.user.create({ + user = await prismaWrite.user.create({ data: { email: loginToken.email, name: loginToken.email.split("@")[0], @@ -51,7 +51,7 @@ export const loader: LoaderFunction = async ({ request }) => { }) } - await prisma.loginToken.delete({ + await prismaWrite.loginToken.delete({ where: { id: loginToken.id, }, @@ -60,7 +60,7 @@ export const loader: LoaderFunction = async ({ request }) => { const ua = new UAParser(data.userAgent) const publicId = nanoid(32) - const accessToken = await prisma.accessToken.create({ + const accessToken = await prismaWrite.accessToken.create({ data: { user: { connect: { @@ -85,7 +85,7 @@ export const loader: LoaderFunction = async ({ request }) => { !nextUrl.hostname.endsWith(`.${OUR_DOMAIN}`) ) { // Check if the host belong to a site - const existing = await prisma.domain.findUnique({ + const existing = await prismaRead.domain.findUnique({ where: { domain: nextUrl.hostname, }, diff --git a/app/routes/api/pages/$id.ts b/app/routes/api/pages/$id.ts index 2ed422e9..5ce42d7d 100644 --- a/app/routes/api/pages/$id.ts +++ b/app/routes/api/pages/$id.ts @@ -1,7 +1,7 @@ import type { ActionFunction } from "@remix-run/node" import { z } from "zod" import { getAuthUser } from "~/lib/auth.server" -import { prisma } from "~/lib/db.server" +import { prismaRead, prismaWrite } from "~/lib/db.server" import { createGate } from "~/lib/gate.server" export const action: ActionFunction = async ({ request, params }) => { @@ -17,7 +17,7 @@ export const action: ActionFunction = async ({ request, params }) => { id: params.id, }) - const page = await prisma.page.findUnique({ + const page = await prismaRead.page.findUnique({ where: { id: data.id, }, @@ -31,7 +31,7 @@ export const action: ActionFunction = async ({ request, params }) => { throw gate.permissionError() } - await prisma.page.update({ + await prismaWrite.page.update({ where: { id: page.id, }, diff --git a/app/routes/api/request-login-link.ts b/app/routes/api/request-login-link.ts index 4c20f68c..8cd44d29 100644 --- a/app/routes/api/request-login-link.ts +++ b/app/routes/api/request-login-link.ts @@ -2,7 +2,7 @@ import { ActionFunction, redirect } from "@remix-run/node" import dayjs from "dayjs" import { z } from "zod" import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared" -import { prisma } from "~/lib/db.server" +import { prismaWrite } from "~/lib/db.server" import { sendLoginEmail } from "~/lib/mailgun.server" export const action: ActionFunction = async ({ request }) => { @@ -17,7 +17,7 @@ export const action: ActionFunction = async ({ request }) => { referer: request.headers.get("referer"), }) - const token = await prisma.loginToken.create({ + const token = await prismaWrite.loginToken.create({ data: { email: input.email, expiresAt: dayjs().add(10, "minute").toDate(),