Skip to content

Commit 1cb87d4

Browse files
authored
Merge branch 'main' into release/v0.10.12
2 parents f1bde09 + 5798603 commit 1cb87d4

6 files changed

Lines changed: 221 additions & 19 deletions

File tree

‎docs/content/docs/2.database/1.index.md‎

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,51 @@ Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database
1515

1616
::tabs{sync="database-dialect"}
1717
:::tabs-item{label="PostgreSQL" icon="i-simple-icons-postgresql"}
18-
:pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"}
19-
::callout
20-
NuxtHub automatically detects your database connection using environment variables:
21-
- Uses `PGlite` (embedded PostgreSQL) if no environment variables are set.
22-
- Uses `postgres-js` driver if you set `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable.
23-
- Use `neon-http` driver with `@neondatabase/serverless` for [Neon](https://neon.com) serverless PostgreSQL.
24-
::
18+
::::tabs{sync="postgres-driver"}
19+
:::::tabs-item{label="postgres-js" icon="i-simple-icons-postgresql"}
20+
:pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"}
21+
22+
::::::callout
23+
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.
24+
::::::
25+
:::::
26+
27+
:::::tabs-item{label="node-postgres" icon="i-simple-icons-nodedotjs"}
28+
:pm-install{name="drizzle-orm drizzle-kit pg"}
29+
:pm-install{name="@types/pg" dev}
30+
31+
::::::callout
32+
Install this when you explicitly set `driver: 'node-postgres'`. This driver uses `pg` instead of `postgres` and does not use the PGlite fallback.
33+
::::::
34+
:::::
35+
36+
:::::tabs-item{label="neon-http" icon="i-logos-neon-icon"}
37+
:pm-install{name="drizzle-orm drizzle-kit @neondatabase/serverless"}
38+
39+
::::::callout
40+
Use this with `driver: 'neon-http'` for Neon serverless PostgreSQL over HTTP.
41+
::::::
42+
:::::
43+
::::
2544
:::
2645
:::tabs-item{label="MySQL" icon="i-simple-icons-mysql"}
2746
:pm-install{name="drizzle-orm drizzle-kit mysql2"}
28-
::callout
47+
::::callout
2948
NuxtHub automatically detects your database connection using environment variables:
3049
- Uses `mysql2` driver if you set `DATABASE_URL` or `MYSQL_URL` environment variable.
3150
- Requires environment variable (no local fallback).
32-
::
51+
::::
3352
:::
3453
:::tabs-item{label="SQLite" icon="i-simple-icons-sqlite"}
3554
:pm-install{name="drizzle-orm drizzle-kit @libsql/client"}
36-
::callout
55+
::::callout
3756
NuxtHub automatically detects your database connection using environment variables:
3857
- Uses `libsql` driver for [Turso](https://turso.tech) if you set `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` environment variables.
3958
- Uses `libsql` locally with file at `.data/db/sqlite.db` if no environment variables are set.
40-
::
41-
::tip{to="/docs/getting-started/deploy#cloudflare"}
59+
::::
60+
::::tip{to="/docs/getting-started/deploy#cloudflare"}
4261
For Cloudflare D1, configure the database ID in your `nuxt.config.ts` and NuxtHub auto-generates the wrangler bindings.
43-
::
62+
::::
4463
:::
4564
::
4665

@@ -322,6 +341,32 @@ This driver requires the following environment variable:
322341
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`.
323342
::
324343

344+
### Node Postgres
345+
346+
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.
347+
348+
```ts [nuxt.config.ts]
349+
export default defineNuxtConfig({
350+
hub: {
351+
db: {
352+
dialect: 'postgresql',
353+
driver: 'node-postgres'
354+
}
355+
}
356+
})
357+
```
358+
359+
Install the required dependencies:
360+
361+
:pm-install{name="drizzle-orm drizzle-kit pg"}
362+
:pm-install{name="@types/pg" dev}
363+
364+
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`. Otherwise, `pg` reads the `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, and `PGPASSWORD` environment variables at runtime.
365+
366+
::note
367+
Read replicas and Cloudflare Hyperdrive are not supported with `node-postgres` yet. Use `postgres-js` for them.
368+
::
369+
325370
### Docker/Kubernetes Deployments
326371

327372
Build container images without database credentials by deferring environment resolution to runtime. Set the driver explicitly and disable automatic migrations during build and dev:
@@ -347,7 +392,7 @@ Build container images without database credentials by deferring environment res
347392
hub: {
348393
db: {
349394
dialect: 'postgresql',
350-
driver: 'postgres-js', // or 'neon-http'
395+
driver: 'postgres-js', // or 'neon-http' or 'node-postgres'
351396
applyMigrationsDuringBuild: false,
352397
applyMigrationsDuringDev: false
353398
}

‎src/db/lib/client.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ export async function createDrizzleClient(config: ResolvedDatabaseConfig, hubDir
4747
pkg = 'drizzle-orm/postgres-js'
4848
const { drizzle } = await import(pkg)
4949
return drizzle({ connection: { ...connection, onnotice: () => {} }, casing })
50+
} else if (driver === 'node-postgres') {
51+
pkg = 'drizzle-orm/node-postgres'
52+
const { drizzle } = await import(pkg)
53+
const { url, ...connectionOptions } = connection
54+
return drizzle({
55+
connection: {
56+
...connectionOptions,
57+
connectionString: connection.connectionString || url
58+
},
59+
casing
60+
})
5061
} else if (driver === 'neon-http') {
5162
const clientPkg = '@neondatabase/serverless'
5263
const { neon } = await import(clientPkg)

‎src/db/setup.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,15 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
110110
break
111111
}
112112
// Only error at build time if migrations need to run
113-
if (config.applyMigrationsDuringBuild && config.driver && ['neon-http', 'postgres-js'].includes(config.driver) && !config.connection.url) {
113+
const hasPostgresConnectionUrl = config.connection.url || (config.driver === 'node-postgres' && config.connection.connectionString)
114+
if (config.applyMigrationsDuringBuild && config.driver && ['neon-http', 'postgres-js', 'node-postgres'].includes(config.driver) && !hasPostgresConnectionUrl) {
114115
throw new Error(`\`${config.driver}\` driver requires \`DATABASE_URL\`, \`POSTGRES_URL\`, or \`POSTGRESQL_URL\` environment variable when \`applyMigrationsDuringBuild\` is enabled`)
115116
}
116-
if (config.connection.url) {
117+
if (hasPostgresConnectionUrl) {
117118
config.driver ||= 'postgres-js'
118119
break
119120
}
121+
if (config.driver === 'node-postgres') break
120122
config.driver ||= 'pglite'
121123
config.connection = defu(config.connection, { dataDir: join(hub.dir, 'db/pglite') })
122124
await mkdir(join(hub.dir, 'db/pglite'), { recursive: true })
@@ -171,6 +173,8 @@ export async function setupDatabase(nuxt: Nuxt, hub: HubConfig, deps: Record<str
171173
}
172174
if (driver === 'postgres-js' && !deps['postgres']) {
173175
logWhenReady(nuxt, 'Please run `npx nypm i postgres` to use PostgreSQL as database.', 'error')
176+
} else if (driver === 'node-postgres' && !deps.pg) {
177+
logWhenReady(nuxt, 'Please run `npx nypm i pg` to use node-postgres as database.', 'error')
174178
} else if (driver === 'neon-http' && !deps['@neondatabase/serverless']) {
175179
logWhenReady(nuxt, 'Please run `npx nypm i @neondatabase/serverless` to use Neon serverless database.', 'error')
176180
} else if (driver === 'pglite' && !deps['@electric-sql/pglite']) {
@@ -357,6 +361,15 @@ async function setupDatabaseClient(nuxt: Nuxt, hub: ResolvedHubConfig) {
357361
: '{ onnotice: () => {}, prepare: false }'
358362
})()
359363

364+
const nodePostgresConnection = (() => {
365+
if (driver !== 'node-postgres' || !connection) return undefined
366+
const { url, ...connectionOptions } = connection as Record<string, unknown>
367+
return {
368+
...connectionOptions,
369+
connectionString: connection.connectionString || url
370+
}
371+
})()
372+
360373
// For types, d1-http uses sqlite-proxy
361374
const driverForTypes = driver === 'd1-http' ? 'sqlite-proxy' : driver
362375

@@ -467,6 +480,17 @@ export { db, schema }
467480
_db = drizzle(sql, { schema${casingOption} })`
468481
)
469482
}
483+
if (driver === 'node-postgres') {
484+
const urlExpr = connection.connectionString || connection.url ? JSON.stringify(connection.connectionString || connection.url) : `process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL`
485+
const { connectionString: _, ...connectionOptions } = (nodePostgresConnection || {}) as Record<string, unknown>
486+
const connectionOptionsExpr = JSON.stringify(connectionOptions)
487+
drizzleOrmContent = generateLazyDbTemplate(
488+
`import { drizzle } from 'drizzle-orm/node-postgres'`,
489+
` const connectionString = ${urlExpr}
490+
if (!connectionString && !process.env.PGHOST && !process.env.PGPORT && !process.env.PGDATABASE && !process.env.PGUSER && !process.env.PGPASSWORD) throw new Error('DATABASE_URL, POSTGRES_URL, or POSTGRESQL_URL required')
491+
_db = drizzle({ connection: { ...${connectionOptionsExpr}, connectionString }, schema${casingOption} })`
492+
)
493+
}
470494
if (driver === 'd1') {
471495
drizzleOrmContent = generateLazyDbTemplate(
472496
`import { drizzle } from 'drizzle-orm/d1'`,

‎src/types/config.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,10 @@ export type DatabaseConfig = {
179179
* Database driver (optional, auto-detected if not provided)
180180
*
181181
* SQLite drivers: 'better-sqlite3', 'libsql', 'bun-sqlite', 'd1', 'd1-http'
182-
* PostgreSQL drivers: 'postgres-js', 'pglite', 'neon-http'
182+
* PostgreSQL drivers: 'postgres-js', 'node-postgres', 'pglite', 'neon-http'
183183
* MySQL drivers: 'mysql2'
184184
*/
185-
driver?: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'pglite' | 'neon-http' | 'mysql2'
185+
driver?: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'node-postgres' | 'pglite' | 'neon-http' | 'mysql2'
186186
/**
187187
* Database connection configuration
188188
*/
@@ -239,7 +239,7 @@ export type DatabaseConfig = {
239239

240240
export type ResolvedDatabaseConfig = DatabaseConfig & {
241241
dialect: 'sqlite' | 'postgresql' | 'mysql'
242-
driver: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'pglite' | 'neon-http' | 'mysql2'
242+
driver: 'better-sqlite3' | 'libsql' | 'bun-sqlite' | 'd1' | 'd1-http' | 'postgres-js' | 'node-postgres' | 'pglite' | 'neon-http' | 'mysql2'
243243
connection: DatabaseConnection
244244
migrationsDirs: string[]
245245
queriesPaths: string[]

‎test/database.client.test.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { createDrizzleClient } from '../src/db/lib/client'
3+
import type { ResolvedDatabaseConfig } from '../src/types'
4+
5+
const { nodePostgresDrizzle } = vi.hoisted(() => ({
6+
nodePostgresDrizzle: vi.fn(() => ({ driver: 'node-postgres' }))
7+
}))
8+
9+
vi.mock('drizzle-orm/node-postgres', () => ({
10+
drizzle: nodePostgresDrizzle
11+
}))
12+
13+
describe('createDrizzleClient', () => {
14+
beforeEach(() => {
15+
nodePostgresDrizzle.mockClear()
16+
})
17+
18+
it('should create node-postgres clients with drizzle-orm/node-postgres', async () => {
19+
const config: ResolvedDatabaseConfig = {
20+
dialect: 'postgresql',
21+
driver: 'node-postgres',
22+
connection: {
23+
connectionString: 'postgresql://user:pass@localhost:5432/db',
24+
ssl: true
25+
},
26+
migrationsDirs: [],
27+
queriesPaths: [],
28+
applyMigrationsDuringBuild: true,
29+
applyMigrationsDuringDev: true,
30+
casing: 'snake_case'
31+
}
32+
33+
const client = await createDrizzleClient(config, '/tmp/hub')
34+
35+
expect(client).toEqual({ driver: 'node-postgres' })
36+
expect(nodePostgresDrizzle).toHaveBeenCalledWith({
37+
connection: {
38+
connectionString: 'postgresql://user:pass@localhost:5432/db',
39+
ssl: true
40+
},
41+
casing: 'snake_case'
42+
})
43+
})
44+
45+
it('should map connection url to node-postgres connectionString', async () => {
46+
const config: ResolvedDatabaseConfig = {
47+
dialect: 'postgresql',
48+
driver: 'node-postgres',
49+
connection: {
50+
url: 'postgresql://user:pass@localhost:5432/db',
51+
ssl: true
52+
},
53+
migrationsDirs: [],
54+
queriesPaths: [],
55+
applyMigrationsDuringBuild: true,
56+
applyMigrationsDuringDev: true
57+
}
58+
59+
await createDrizzleClient(config, '/tmp/hub')
60+
61+
expect(nodePostgresDrizzle).toHaveBeenCalledWith({
62+
connection: {
63+
connectionString: 'postgresql://user:pass@localhost:5432/db',
64+
ssl: true
65+
}
66+
})
67+
})
68+
})

‎test/database.config.test.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,44 @@ describe('resolveDatabaseConfig', () => {
311311
})
312312
})
313313

314+
it('should use node-postgres driver when explicitly set', async () => {
315+
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db'
316+
317+
const nuxt = createMockNuxt()
318+
const hub = createBaseHubConfig({
319+
dialect: 'postgresql',
320+
driver: 'node-postgres'
321+
})
322+
323+
const result = await resolveDatabaseConfig(nuxt, hub)
324+
325+
expect(result).toMatchObject({
326+
dialect: 'postgresql',
327+
driver: 'node-postgres',
328+
connection: {
329+
url: 'postgresql://user:pass@localhost:5432/db'
330+
}
331+
})
332+
})
333+
334+
it('should allow node-postgres without DATABASE_URL when applyMigrationsDuringBuild is false', async () => {
335+
const nuxt = createMockNuxt()
336+
const hub = createBaseHubConfig({
337+
dialect: 'postgresql',
338+
driver: 'node-postgres',
339+
applyMigrationsDuringBuild: false
340+
})
341+
342+
const result = await resolveDatabaseConfig(nuxt, hub)
343+
344+
expect(result).toMatchObject({
345+
dialect: 'postgresql',
346+
driver: 'node-postgres',
347+
connection: { url: '' },
348+
applyMigrationsDuringBuild: false
349+
})
350+
})
351+
314352
it('should not use neon-http driver automatically', async () => {
315353
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db'
316354

@@ -341,6 +379,22 @@ describe('resolveDatabaseConfig', () => {
341379
)
342380
})
343381

382+
it('should not treat connectionString as DATABASE_URL for neon-http', async () => {
383+
const nuxt = createMockNuxt()
384+
const hub = createBaseHubConfig({
385+
dialect: 'postgresql',
386+
driver: 'neon-http',
387+
connection: {
388+
connectionString: 'postgresql://user:pass@localhost:5432/db'
389+
},
390+
applyMigrationsDuringBuild: true
391+
})
392+
393+
await expect(resolveDatabaseConfig(nuxt, hub)).rejects.toThrow(
394+
'`neon-http` driver requires `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable when `applyMigrationsDuringBuild` is enabled'
395+
)
396+
})
397+
344398
it('should allow neon-http without DATABASE_URL when applyMigrationsDuringBuild is false', async () => {
345399
const nuxt = createMockNuxt()
346400
const hub = createBaseHubConfig({

0 commit comments

Comments
 (0)