From 1fa640cd4a20ae3de09e00f5b3b7b3e676e112e2 Mon Sep 17 00:00:00 2001 From: tanchaowen84 Date: Wed, 20 May 2026 12:18:56 +0800 Subject: [PATCH] Default generate to PGlite --- README.md | 4 +- src/cli.ts | 12 +++- src/generate.ts | 130 +++++++++++++++++++--------------- tests/generate.pglite.test.ts | 87 +++++++++++++++++------ 4 files changed, 152 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 20e3127..a4b6b54 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm version](https://badge.fury.io/js/pgstrap.svg)](https://badge.fury.io/js/pgstrap) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -pgstrap allows you to easily run typescript migrations or generate a directory that represents your database schemas with `table.sql` files. Run `pgstrap generate` to generate a directory with the structure of your postgres database schemas! +pgstrap allows you to easily run typescript migrations or generate a directory that represents your database schemas with `table.sql` files. Run `pgstrap generate` to generate a directory with the structure of your postgres database schemas. Type generation uses PGlite by default, so Postgres does not need to be running locally. ## Features @@ -55,7 +55,7 @@ npm install pgstrap --save-dev - `npm run db:migrate` - Run pending migrations - `npm run db:reset` - Drop and recreate the database, then run all migrations -- `npm run db:generate` - Generate types and structure dumps. Use `pgstrap generate --pglite` to run migrations against an in-memory PGlite instance. +- `npm run db:generate` - Generate types and structure dumps with an in-memory PGlite database by default. Use `pgstrap generate --no-pglite` to generate from an external Postgres database configured by `DATABASE_URL`. - `npm run db:create-migration` - Create a new migration file ### Configuration diff --git a/src/cli.ts b/src/cli.ts index 9a9bdec..8fc76a9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -35,10 +35,18 @@ import { getProjectContext } from "./get-project-context" "generate", "generate types and sql documentation from database", (yargs) => { - yargs.option("pglite", { type: "boolean", default: false }) + yargs.option("pglite", { + type: "boolean", + default: true, + describe: + "run migrations in an in-memory PGlite database before generating types", + }) }, async (argv) => { - generate({ ...(await getProjectContext()), pglite: !!argv.pglite }) + await generate({ + ...(await getProjectContext()), + pglite: argv.pglite !== false, + }) }, ) .parse() diff --git a/src/generate.ts b/src/generate.ts index f337094..a774d4c 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,18 +1,28 @@ import * as zg from "zapatos/generate" -import { - getConnectionStringFromEnv, - getPgConnectionFromEnv, -} from "pg-connection-from-env" +import { getConnectionStringFromEnv } from "pg-connection-from-env" import { Context } from "./get-project-context" import { dumpTree } from "pg-schema-dump" import path from "path" import { migrate } from "./migrate" +const restoreDatabaseUrl = (previousDatabaseUrl: string | undefined) => { + if (previousDatabaseUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = previousDatabaseUrl +} + +const closeServer = async (server: import("node:net").Server) => { + if (!server.listening) return + + await new Promise((resolve) => { + server.close(() => resolve()) + }) +} + export const generate = async ({ schemas, defaultDatabase, dbDir, - pglite = false, + pglite = true, migrationsDir, }: Pick & { pglite?: boolean @@ -27,65 +37,71 @@ export const generate = async ({ const net = await import("node:net") const db = new PGlite() + const previousDatabaseUrl = process.env.DATABASE_URL + let server: import("node:net").Server | undefined - await migrate({ - client: db as any, - migrationsDir, - defaultDatabase, - cwd: process.cwd(), - schemas, - }) - - const server = net.createServer(async (socket) => { - const connection = await fromNodeSocket(socket, { - serverVersion: "16.3 (PGlite)", - auth: { - method: "password", - validateCredentials: ({ username, password }: any) => - username === "postgres" && password === "postgres", - getClearTextPassword: () => "postgres", - }, - async onStartup() { - await (db as any).waitReady - }, - async onMessage(data: Uint8Array, { isAuthenticated }: any) { - if (!isAuthenticated) return - try { - const { data: responseData } = await (db as any).execProtocol(data) - return responseData - } catch { - return undefined - } - }, + try { + await migrate({ + client: db as any, + migrationsDir, + defaultDatabase, + cwd: process.cwd(), + schemas, }) - }) - await new Promise((resolve) => server.listen(0, resolve)) - const port = (server.address() as any).port - const connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/postgres` + const activeServer = net.createServer(async (socket) => { + await fromNodeSocket(socket, { + serverVersion: "16.3 (PGlite)", + auth: { + method: "password", + validateCredentials: ({ username, password }: any) => + username === "postgres" && password === "postgres", + getClearTextPassword: () => "postgres", + }, + async onStartup() { + await (db as any).waitReady + }, + async onMessage(data: Uint8Array, { isAuthenticated }: any) { + if (!isAuthenticated) return + try { + const { data: responseData } = await (db as any).execProtocol( + data, + ) + return responseData + } catch { + return undefined + } + }, + }) + }) + server = activeServer - const prevDbUrl = process.env.DATABASE_URL - process.env.DATABASE_URL = connectionString + await new Promise((resolve) => activeServer.listen(0, resolve)) + const port = (activeServer.address() as any).port + const connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/postgres` - await zg.generate({ - db: { - connectionString, - }, - schemas: Object.fromEntries( - schemas.map((s) => [s, { include: "*", exclude: [] }]), - ), - outDir: dbDir, - }) + process.env.DATABASE_URL = connectionString - await dumpTree({ - targetDir: path.join(dbDir, "structure"), - defaultDatabase: "postgres", - schemas, - }) + await zg.generate({ + db: { + connectionString, + }, + schemas: Object.fromEntries( + schemas.map((s) => [s, { include: "*", exclude: [] }]), + ), + outDir: dbDir, + }) - server.close() - if (prevDbUrl === undefined) delete process.env.DATABASE_URL - else process.env.DATABASE_URL = prevDbUrl + await dumpTree({ + targetDir: path.join(dbDir, "structure"), + defaultDatabase: "postgres", + schemas, + }) + } finally { + restoreDatabaseUrl(previousDatabaseUrl) + if (server) await closeServer(server) + await (db as any).close?.() + } return } diff --git a/tests/generate.pglite.test.ts b/tests/generate.pglite.test.ts index 56dcd53..a99b30f 100644 --- a/tests/generate.pglite.test.ts +++ b/tests/generate.pglite.test.ts @@ -13,35 +13,82 @@ exports.down = async (pgm) => { } ` -test("generate with pglite runs migrations and dumps structure", async () => { +test("generate defaults to pglite and dumps structure without postgres", async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pgstrap-generate-")) const migrationsDir = path.join(tmp, "migrations") + const previousDatabaseUrl = process.env.DATABASE_URL + fs.mkdirSync(migrationsDir, { recursive: true }) fs.writeFileSync( path.join(migrationsDir, "001_create_table.js"), migrationFile, ) - await generate({ - schemas: ["public"], - defaultDatabase: "postgres", - dbDir: path.join(tmp, "db"), - migrationsDir, - pglite: true, - }) - - const zapatosFile = path.join(tmp, "db", "zapatos", "schema.d.ts") - const structureDir = path.join( - tmp, - "db", - "structure", - "public", - "tables", - "foo", + process.env.DATABASE_URL = "postgres://postgres:postgres@127.0.0.1:1/missing" + + try { + await generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir: path.join(tmp, "db"), + migrationsDir, + }) + + const zapatosFile = path.join(tmp, "db", "zapatos", "schema.d.ts") + const structureDir = path.join( + tmp, + "db", + "structure", + "public", + "tables", + "foo", + ) + + expect(fs.existsSync(zapatosFile)).toBe(true) + expect(fs.existsSync(path.join(structureDir, "table.sql"))).toBe(true) + expect(process.env.DATABASE_URL).toBe( + "postgres://postgres:postgres@127.0.0.1:1/missing", + ) + } finally { + if (previousDatabaseUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = previousDatabaseUrl + + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + +test("generate with pglite restores DATABASE_URL after failure", async () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pgstrap-generate-")) + const migrationsDir = path.join(tmp, "migrations") + const dbDir = path.join(tmp, "db") + const previousDatabaseUrl = process.env.DATABASE_URL + + fs.mkdirSync(migrationsDir, { recursive: true }) + fs.writeFileSync( + path.join(migrationsDir, "001_create_table.js"), + migrationFile, ) + fs.writeFileSync(dbDir, "not a directory") + + process.env.DATABASE_URL = "postgres://existing:secret@localhost:5432/app" + + try { + await expect( + generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir, + migrationsDir, + }), + ).rejects.toThrow() - expect(fs.existsSync(zapatosFile)).toBe(true) - expect(fs.existsSync(path.join(structureDir, "table.sql"))).toBe(true) + expect(process.env.DATABASE_URL).toBe( + "postgres://existing:secret@localhost:5432/app", + ) + } finally { + if (previousDatabaseUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = previousDatabaseUrl - fs.rmSync(tmp, { recursive: true, force: true }) + fs.rmSync(tmp, { recursive: true, force: true }) + } })