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.ts b/src/server/prisma.ts deleted file mode 100644 index f6d9c92a..00000000 --- a/src/server/prisma.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Instantiates a single instance PrismaClient and save it on the global object. - * @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices - */ -import { PrismaClient } from '@prisma/client' -import { env } from '~/env.mjs' - -const prismaGlobal = global as typeof global & { - prisma?: PrismaClient -} - -export const prisma: PrismaClient = - prismaGlobal.prisma || - new PrismaClient({ - log: env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'], - }) - -if (env.NODE_ENV !== 'production') { - prismaGlobal.prisma = prisma -} diff --git a/src/server/prisma/index.ts b/src/server/prisma/index.ts new file mode 100644 index 00000000..9b5ecb2b --- /dev/null +++ b/src/server/prisma/index.ts @@ -0,0 +1,39 @@ +/** + * Instantiates a single instance PrismaClient and save it on the global object. + * @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 +} + +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 +} 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