diff --git a/docs/content/docs/2.database/1.index.md b/docs/content/docs/2.database/1.index.md index 2e7ac555b..700e1448e 100644 --- a/docs/content/docs/2.database/1.index.md +++ b/docs/content/docs/2.database/1.index.md @@ -15,32 +15,51 @@ Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database ::tabs{sync="database-dialect"} :::tabs-item{label="PostgreSQL" icon="i-simple-icons-postgresql"} - :pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"} - ::callout - NuxtHub automatically detects your database connection using environment variables: - - Uses `PGlite` (embedded PostgreSQL) if no environment variables are set. - - Uses `postgres-js` driver if you set `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable. - - Use `neon-http` driver with `@neondatabase/serverless` for [Neon](https://neon.com) serverless PostgreSQL. - :: + ::::tabs{sync="postgres-driver"} + :::::tabs-item{label="postgres-js" icon="i-simple-icons-postgresql"} + :pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"} + + ::::::callout + Install this for NuxtHub's default PostgreSQL auto-detection. NuxtHub uses `postgres-js` when `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` is set, and uses `@electric-sql/pglite` as the local fallback when no connection URL is set. + :::::: + ::::: + + :::::tabs-item{label="node-postgres" icon="i-simple-icons-nodedotjs"} + :pm-install{name="drizzle-orm drizzle-kit pg"} + :pm-install{name="@types/pg" dev} + + ::::::callout + Install this when you explicitly set `driver: 'node-postgres'`. This driver uses `pg` instead of `postgres` and does not use the PGlite fallback. + :::::: + ::::: + + :::::tabs-item{label="neon-http" icon="i-logos-neon-icon"} + :pm-install{name="drizzle-orm drizzle-kit @neondatabase/serverless"} + + ::::::callout + Use this with `driver: 'neon-http'` for Neon serverless PostgreSQL over HTTP. + :::::: + ::::: + :::: ::: :::tabs-item{label="MySQL" icon="i-simple-icons-mysql"} :pm-install{name="drizzle-orm drizzle-kit mysql2"} - ::callout + ::::callout NuxtHub automatically detects your database connection using environment variables: - Uses `mysql2` driver if you set `DATABASE_URL` or `MYSQL_URL` environment variable. - Requires environment variable (no local fallback). - :: + :::: ::: :::tabs-item{label="SQLite" icon="i-simple-icons-sqlite"} :pm-install{name="drizzle-orm drizzle-kit @libsql/client"} - ::callout + ::::callout NuxtHub automatically detects your database connection using environment variables: - Uses `libsql` driver for [Turso](https://turso.tech) if you set `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` environment variables. - Uses `libsql` locally with file at `.data/db/sqlite.db` if no environment variables are set. - :: - ::tip{to="/docs/getting-started/deploy#cloudflare"} + :::: + ::::tip{to="/docs/getting-started/deploy#cloudflare"} For Cloudflare D1, configure the database ID in your `nuxt.config.ts` and NuxtHub auto-generates the wrangler bindings. - :: + :::: ::: :: @@ -326,6 +345,28 @@ This driver requires the following environment variable: You can find your Neon connection string in the [Neon dashboard](https://console.neon.tech). The connection string format is `postgresql://user:password@hostname/database`. :: +### Node Postgres + +Use the `node-postgres` driver to connect with Drizzle's [`drizzle-orm/node-postgres`](https://orm.drizzle.team/docs/connect-planetscale-postgres#node-postgres) driver. This is useful for standard PostgreSQL clients, including PlanetScale Postgres. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + hub: { + db: { + dialect: 'postgresql', + driver: 'node-postgres' + } + } +}) +``` + +Install the required dependencies: + +:pm-install{name="drizzle-orm drizzle-kit pg"} +:pm-install{name="@types/pg" dev} + +NuxtHub does not auto-detect `node-postgres`; set `driver: 'node-postgres'` explicitly. When `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` is set, NuxtHub passes it to Drizzle as the `pg` `connectionString`. + ### Docker/Kubernetes Deployments Build container images without database credentials by deferring environment resolution to runtime. Set the driver explicitly and disable automatic migrations during build and dev: @@ -351,7 +392,7 @@ Build container images without database credentials by deferring environment res hub: { db: { dialect: 'postgresql', - driver: 'postgres-js', // or 'neon-http' + driver: 'postgres-js', // or 'neon-http' or 'node-postgres' applyMigrationsDuringBuild: false, applyMigrationsDuringDev: false } diff --git a/src/db/lib/client.ts b/src/db/lib/client.ts index cfd544780..47c0099dc 100644 --- a/src/db/lib/client.ts +++ b/src/db/lib/client.ts @@ -46,6 +46,17 @@ export async function createDrizzleClient(config: ResolvedDatabaseConfig, hubDir pkg = 'drizzle-orm/postgres-js' const { drizzle } = await import(pkg) return drizzle({ connection: { ...connection, onnotice: () => {} }, casing }) + } else if (driver === 'node-postgres') { + pkg = 'drizzle-orm/node-postgres' + const { drizzle } = await import(pkg) + const { url, ...connectionOptions } = connection + return drizzle({ + connection: { + ...connectionOptions, + connectionString: connection.connectionString || url + }, + casing + }) } else if (driver === 'neon-http') { const clientPkg = '@neondatabase/serverless' const { neon } = await import(clientPkg) diff --git a/src/db/setup.ts b/src/db/setup.ts index 25db19369..c2623c3bf 100644 --- a/src/db/setup.ts +++ b/src/db/setup.ts @@ -108,13 +108,15 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise } config.connection = defu(config.connection, { url: process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL || '' }) // Only error at build time if migrations need to run - if (config.applyMigrationsDuringBuild && config.driver && ['neon-http', 'postgres-js'].includes(config.driver) && !config.connection.url) { + const hasPostgresConnectionUrl = config.connection.url || (config.driver === 'node-postgres' && config.connection.connectionString) + if (config.applyMigrationsDuringBuild && config.driver && ['neon-http', 'postgres-js', 'node-postgres'].includes(config.driver) && !hasPostgresConnectionUrl) { throw new Error(`\`${config.driver}\` driver requires \`DATABASE_URL\`, \`POSTGRES_URL\`, or \`POSTGRESQL_URL\` environment variable when \`applyMigrationsDuringBuild\` is enabled`) } - if (config.connection.url) { + if (hasPostgresConnectionUrl) { config.driver ||= 'postgres-js' break } + if (config.driver === 'node-postgres') break config.driver ||= 'pglite' config.connection = defu(config.connection, { dataDir: join(hub.dir, 'db/pglite') }) await mkdir(join(hub.dir, 'db/pglite'), { recursive: true }) @@ -166,6 +168,8 @@ export async function setupDatabase(nuxt: Nuxt, hub: HubConfig, deps: Record {}, ...${JSON.stringify(rest)} }` : '{ onnotice: () => {} }' })() + const nodePostgresConnection = (() => { + if (driver !== 'node-postgres' || !connection) return undefined + const { url, ...connectionOptions } = connection as Record + return { + ...connectionOptions, + connectionString: connection.connectionString || url + } + })() + // For types, d1-http uses sqlite-proxy const driverForTypes = driver === 'd1-http' ? 'sqlite-proxy' : driver @@ -454,6 +467,17 @@ export { db, schema } _db = drizzle(sql, { schema${casingOption} })` ) } + if (driver === 'node-postgres') { + const urlExpr = connection.connectionString || connection.url ? JSON.stringify(connection.connectionString || connection.url) : `process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL` + const { connectionString: _, ...connectionOptions } = (nodePostgresConnection || {}) as Record + const connectionOptionsExpr = JSON.stringify(connectionOptions) + drizzleOrmContent = generateLazyDbTemplate( + `import { drizzle } from 'drizzle-orm/node-postgres'`, + ` const connectionString = ${urlExpr} + if (!connectionString) throw new Error('DATABASE_URL, POSTGRES_URL, or POSTGRESQL_URL required') + _db = drizzle({ connection: { ...${connectionOptionsExpr}, connectionString }, schema${casingOption} })` + ) + } if (driver === 'd1') { drizzleOrmContent = generateLazyDbTemplate( `import { drizzle } from 'drizzle-orm/d1'`, diff --git a/src/types/config.ts b/src/types/config.ts index c0edaa201..c166c51e7 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -168,10 +168,10 @@ export type DatabaseConfig = { * Database driver (optional, auto-detected if not provided) * * SQLite drivers: 'better-sqlite3', 'libsql', 'bun-sqlite', 'd1', 'd1-http' - * PostgreSQL drivers: 'postgres-js', 'pglite', 'neon-http' + * PostgreSQL drivers: 'postgres-js', 'node-postgres', 'pglite', 'neon-http' * MySQL drivers: 'mysql2' */ - driver?: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'pglite' | 'neon-http' | 'mysql2' + driver?: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'node-postgres' | 'pglite' | 'neon-http' | 'mysql2' /** * Database connection configuration */ @@ -228,7 +228,7 @@ export type DatabaseConfig = { export type ResolvedDatabaseConfig = DatabaseConfig & { dialect: 'sqlite' | 'postgresql' | 'mysql' - driver: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'pglite' | 'neon-http' | 'mysql2' + driver: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'node-postgres' | 'pglite' | 'neon-http' | 'mysql2' connection: DatabaseConnection migrationsDirs: string[] queriesPaths: string[] diff --git a/test/database.client.test.ts b/test/database.client.test.ts new file mode 100644 index 000000000..76e355b9d --- /dev/null +++ b/test/database.client.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createDrizzleClient } from '../src/db/lib/client' +import type { ResolvedDatabaseConfig } from '../src/types' + +const { nodePostgresDrizzle } = vi.hoisted(() => ({ + nodePostgresDrizzle: vi.fn(() => ({ driver: 'node-postgres' })) +})) + +vi.mock('drizzle-orm/node-postgres', () => ({ + drizzle: nodePostgresDrizzle +})) + +describe('createDrizzleClient', () => { + beforeEach(() => { + nodePostgresDrizzle.mockClear() + }) + + it('should create node-postgres clients with drizzle-orm/node-postgres', async () => { + const config: ResolvedDatabaseConfig = { + dialect: 'postgresql', + driver: 'node-postgres', + connection: { + connectionString: 'postgresql://user:pass@localhost:5432/db', + ssl: true + }, + migrationsDirs: [], + queriesPaths: [], + applyMigrationsDuringBuild: true, + applyMigrationsDuringDev: true, + casing: 'snake_case' + } + + const client = await createDrizzleClient(config, '/tmp/hub') + + expect(client).toEqual({ driver: 'node-postgres' }) + expect(nodePostgresDrizzle).toHaveBeenCalledWith({ + connection: { + connectionString: 'postgresql://user:pass@localhost:5432/db', + ssl: true + }, + casing: 'snake_case' + }) + }) + + it('should map connection url to node-postgres connectionString', async () => { + const config: ResolvedDatabaseConfig = { + dialect: 'postgresql', + driver: 'node-postgres', + connection: { + url: 'postgresql://user:pass@localhost:5432/db', + ssl: true + }, + migrationsDirs: [], + queriesPaths: [], + applyMigrationsDuringBuild: true, + applyMigrationsDuringDev: true + } + + await createDrizzleClient(config, '/tmp/hub') + + expect(nodePostgresDrizzle).toHaveBeenCalledWith({ + connection: { + connectionString: 'postgresql://user:pass@localhost:5432/db', + ssl: true + } + }) + }) +}) diff --git a/test/database.config.test.ts b/test/database.config.test.ts index 283c740e5..fb6eb1731 100644 --- a/test/database.config.test.ts +++ b/test/database.config.test.ts @@ -277,6 +277,44 @@ describe('resolveDatabaseConfig', () => { }) }) + it('should use node-postgres driver when explicitly set', async () => { + process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db' + + const nuxt = createMockNuxt() + const hub = createBaseHubConfig({ + dialect: 'postgresql', + driver: 'node-postgres' + }) + + const result = await resolveDatabaseConfig(nuxt, hub) + + expect(result).toMatchObject({ + dialect: 'postgresql', + driver: 'node-postgres', + connection: { + url: 'postgresql://user:pass@localhost:5432/db' + } + }) + }) + + it('should allow node-postgres without DATABASE_URL when applyMigrationsDuringBuild is false', async () => { + const nuxt = createMockNuxt() + const hub = createBaseHubConfig({ + dialect: 'postgresql', + driver: 'node-postgres', + applyMigrationsDuringBuild: false + }) + + const result = await resolveDatabaseConfig(nuxt, hub) + + expect(result).toMatchObject({ + dialect: 'postgresql', + driver: 'node-postgres', + connection: { url: '' }, + applyMigrationsDuringBuild: false + }) + }) + it('should not use neon-http driver automatically', async () => { process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db' @@ -307,6 +345,22 @@ describe('resolveDatabaseConfig', () => { ) }) + it('should not treat connectionString as DATABASE_URL for neon-http', async () => { + const nuxt = createMockNuxt() + const hub = createBaseHubConfig({ + dialect: 'postgresql', + driver: 'neon-http', + connection: { + connectionString: 'postgresql://user:pass@localhost:5432/db' + }, + applyMigrationsDuringBuild: true + }) + + await expect(resolveDatabaseConfig(nuxt, hub)).rejects.toThrow( + '`neon-http` driver requires `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable when `applyMigrationsDuringBuild` is enabled' + ) + }) + it('should allow neon-http without DATABASE_URL when applyMigrationsDuringBuild is false', async () => { const nuxt = createMockNuxt() const hub = createBaseHubConfig({