From b08d62a4969385f0aa6cbf0bb5bf534aeb809134 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Fri, 4 Oct 2024 09:41:07 +0800 Subject: [PATCH 1/2] refactor(pglite): promote to prisma submodule Make pglite part of the core codebase, so that we can use it in environments where postgres may not be available, such as in browser-only environments like StackBlitz. - Rename prisma -> prisma/index to host submodules - Move pglite-related logic to pglite - Wrap actual init of PGlite as factory, and not within module load - Rework vitest.setup to depend on new submodule - Keep resetDb, since that is only useful to testing --- src/server/{prisma.ts => prisma/index.ts} | 0 src/server/prisma/pglite.ts | 25 +++++++++++++++++++++++ vitest.setup.ts | 25 ++++------------------- 3 files changed, 29 insertions(+), 21 deletions(-) rename src/server/{prisma.ts => prisma/index.ts} (100%) create mode 100644 src/server/prisma/pglite.ts diff --git a/src/server/prisma.ts b/src/server/prisma/index.ts similarity index 100% rename from src/server/prisma.ts rename to src/server/prisma/index.ts diff --git a/src/server/prisma/pglite.ts b/src/server/prisma/pglite.ts new file mode 100644 index 00000000..46746574 --- /dev/null +++ b/src/server/prisma/pglite.ts @@ -0,0 +1,25 @@ +import { PGlite } from '@electric-sql/pglite' +import { PrismaPGlite } from 'pglite-prisma-adapter' +import { PrismaClient } from '@prisma/client' +import { readdirSync, readFileSync, statSync } from 'node:fs' + +export const makePgliteClient = () => { + const client = new PGlite() + const adapter = new PrismaPGlite(client) + return { + client, + prisma: new PrismaClient({ adapter }), + } +} + +export const applyMigrations = async (client: PGlite) => { + const prismaMigrationDir = './prisma/migrations' + const directory = readdirSync(prismaMigrationDir).sort() + for (const file of directory) { + const name = `${prismaMigrationDir}/${file}` + if (statSync(name).isDirectory()) { + const migration = readFileSync(`${name}/migration.sql`, 'utf8') + await client.exec(migration) + } + } +} diff --git a/vitest.setup.ts b/vitest.setup.ts index c2853626..1b153b11 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,18 +1,13 @@ import { vi } from 'vitest' -import { PGlite } from '@electric-sql/pglite' -import { PrismaPGlite } from 'pglite-prisma-adapter' -import { PrismaClient } from '@prisma/client' -import { readdirSync, readFileSync, statSync } from 'node:fs' +import { makePgliteClient, applyMigrations } from '~/server/prisma/pglite' -const client = new PGlite() -const adapter = new PrismaPGlite(client) -const prisma = new PrismaClient({ adapter }) +const { client, prisma } = makePgliteClient() vi.mock('./src/server/prisma', () => ({ prisma, })) -export const resetDb = async () => { +const resetDb = async () => { try { await client.exec(`DROP SCHEMA public CASCADE`) await client.exec(`CREATE SCHEMA public`) @@ -21,21 +16,9 @@ export const resetDb = async () => { } } -const applyMigrations = async () => { - const prismaMigrationDir = './prisma/migrations' - const directory = readdirSync(prismaMigrationDir).sort() - for (const file of directory) { - const name = `${prismaMigrationDir}/${file}` - if (statSync(name).isDirectory()) { - const migration = readFileSync(`${name}/migration.sql`, 'utf8') - await client.exec(migration) - } - } -} - // Apply migrations before each test beforeEach(async () => { - await applyMigrations() + await applyMigrations(client) }) // Clean up the database after each test From 07fe036eb6b129e45be661de675aa515625d07c4 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Fri, 4 Oct 2024 10:32:35 +0800 Subject: [PATCH 2/2] feat(pglite): use in dev mode if no db url If `NODE_ENV` is set to `development`, but no `DATABASE_URL` is given, we are likely in an environment which has no database, so fallback on using pglite. --- src/env.mjs | 13 ++++++++++++- src/server/prisma/index.ts | 29 ++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/env.mjs b/src/env.mjs index 34f9ddf3..924cb446 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -75,7 +75,7 @@ const sgidServerSchema = z.discriminatedUnion('NEXT_PUBLIC_ENABLE_SGID', [ */ const server = z .object({ - DATABASE_URL: z.string().url(), + DATABASE_URL: z.string().url().optional(), NODE_ENV: z.enum(['development', 'test', 'production']), OTP_EXPIRY: z.coerce.number().positive().optional().default(600), POSTMAN_API_KEY: z.string().optional(), @@ -122,6 +122,17 @@ const server = z message: 'SENDGRID_FROM_ADDRESS is required when SENDGRID_API_KEY is set', path: ['SENDGRID_FROM_ADDRESS'], }) + .refine( + (val) => + val.NODE_ENV === 'development' || + val.NODE_ENV === 'test' || + val.DATABASE_URL, + { + message: + 'DATABASE_URL has to be set if NODE_ENV is not development or test', + path: ['DATABASE_URL'], + }, + ) /** * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. diff --git a/src/server/prisma/index.ts b/src/server/prisma/index.ts index f6d9c92a..9b5ecb2b 100644 --- a/src/server/prisma/index.ts +++ b/src/server/prisma/index.ts @@ -3,17 +3,36 @@ * @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices */ import { PrismaClient } from '@prisma/client' +import pino from 'pino' import { env } from '~/env.mjs' +import { makePgliteClient, applyMigrations } from './pglite' const prismaGlobal = global as typeof global & { prisma?: PrismaClient } -export const prisma: PrismaClient = - prismaGlobal.prisma || - new PrismaClient({ - log: env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'], - }) +const choosePrismaClient = () => { + if ( + !env.DATABASE_URL && + (env.NODE_ENV === 'development' || env.NODE_ENV === 'test') + ) { + pino().warn({}, 'DATABASE_URL not set, using pglite') + const { client, prisma: pglitePrismaClient } = makePgliteClient() + // Inject an env var to appease Prisma + process.env.DATABASE_URL = 'postgres://using:pglite@localhost:5432/' + void applyMigrations(client) + return pglitePrismaClient + } else { + return ( + prismaGlobal.prisma || + new PrismaClient({ + log: env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'], + }) + ) + } +} + +export const prisma: PrismaClient = choosePrismaClient() if (env.NODE_ENV !== 'production') { prismaGlobal.prisma = prisma