From c13d920fbfc1ed964dd3e0984c5c0634a15854f3 Mon Sep 17 00:00:00 2001 From: Axile Date: Thu, 21 May 2026 15:23:32 +0800 Subject: [PATCH] Default generate to PGlite --- README.md | 5 +- src/cli.ts | 14 +++- src/generate.ts | 142 ++++++++++++++++++++-------------- src/init.ts | 2 +- tests/generate.pglite.test.ts | 55 +++++++++++-- tests/init.test.ts | 2 +- 6 files changed, 147 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 20e3127..a729ebf 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ npm install pgstrap --save-dev npm run db:migrate ``` -6. Generate types and structure: +6. Generate types and structure without running a local Postgres server: ```bash npm run db:generate ``` @@ -55,7 +55,8 @@ 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 against an in-memory PGlite instance by default. +- `pgstrap generate --no-pglite` - Generate from `DATABASE_URL` or the configured Postgres defaults instead of PGlite. - `npm run db:create-migration` - Create a new migration file ### Configuration diff --git a/src/cli.ts b/src/cli.ts index 9a9bdec..bea4678 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", { + describe: + "run migrations in an in-memory PGlite database before generating output", + type: "boolean", + default: true, + }) }, - async (argv) => { - generate({ ...(await getProjectContext()), pglite: !!argv.pglite }) + async (argv): Promise => { + await generate({ + ...(await getProjectContext()), + pglite: argv.pglite !== false, + }) }, ) .parse() diff --git a/src/generate.ts b/src/generate.ts index f337094..4b38066 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,9 +1,7 @@ import * as zg from "zapatos/generate" -import { - getConnectionStringFromEnv, - getPgConnectionFromEnv, -} from "pg-connection-from-env" -import { Context } from "./get-project-context" +import { getConnectionStringFromEnv } from "pg-connection-from-env" +import type { AddressInfo } from "node:net" +import type { Context } from "./get-project-context" import { dumpTree } from "pg-schema-dump" import path from "path" import { migrate } from "./migrate" @@ -12,12 +10,12 @@ export const generate = async ({ schemas, defaultDatabase, dbDir, - pglite = false, + pglite = true, migrationsDir, }: Pick & { pglite?: boolean migrationsDir?: string -}) => { +}): Promise => { dbDir = dbDir ?? "./src/db" migrationsDir = migrationsDir ?? path.join(dbDir, "migrations") @@ -27,65 +25,93 @@ export const generate = async ({ const net = await import("node:net") const db = new PGlite() + const prevDbUrl = process.env.DATABASE_URL + let server: ReturnType | undefined - await migrate({ - client: db as any, - migrationsDir, - defaultDatabase, - cwd: process.cwd(), - schemas, - }) + try { + 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 - } - }, + const pgliteServer = net.createServer(async (socket) => { + await fromNodeSocket(socket, { + serverVersion: "16.3 (PGlite)", + auth: { + method: "password", + validateCredentials: ({ + username, + password, + }: { + username: string + password: string + }) => username === "postgres" && password === "postgres", + getClearTextPassword: () => "postgres", + }, + async onStartup(): Promise { + await (db as any).waitReady + }, + async onMessage( + data: Uint8Array, + { isAuthenticated }: { isAuthenticated: boolean }, + ): Promise { + if (!isAuthenticated) return undefined + try { + const { data: responseData } = await (db as any).execProtocol( + data, + ) + return responseData + } catch { + return undefined + } + }, + }) }) - }) + server = pgliteServer - 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` + await new Promise((resolve) => pgliteServer.listen(0, resolve)) + const address = pgliteServer.address() as AddressInfo + const connectionString = `postgres://postgres:postgres@127.0.0.1:${address.port}/postgres` - const prevDbUrl = process.env.DATABASE_URL - process.env.DATABASE_URL = connectionString + process.env.DATABASE_URL = connectionString + + await zg.generate({ + db: { + connectionString, + }, + schemas: Object.fromEntries( + schemas.map((s) => [s, { include: "*", exclude: [] }]), + ), + outDir: dbDir, + }) + + await dumpTree({ + targetDir: path.join(dbDir, "structure"), + defaultDatabase: "postgres", + schemas, + }) + } finally { + const openServer = server - await zg.generate({ - db: { - connectionString, - }, - schemas: Object.fromEntries( - schemas.map((s) => [s, { include: "*", exclude: [] }]), - ), - outDir: dbDir, - }) + if (openServer) { + await new Promise((resolve, reject) => { + openServer.close((error?: Error) => { + if (error) { + reject(error) + return + } - await dumpTree({ - targetDir: path.join(dbDir, "structure"), - defaultDatabase: "postgres", - schemas, - }) + resolve() + }) + }) + } - server.close() - if (prevDbUrl === undefined) delete process.env.DATABASE_URL - else process.env.DATABASE_URL = prevDbUrl + if (prevDbUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = prevDbUrl + } return } diff --git a/src/init.ts b/src/init.ts index b84f9cd..e396f28 100644 --- a/src/init.ts +++ b/src/init.ts @@ -16,7 +16,7 @@ export const initPgstrap = async (ctx: Pick) => { pkg.scripts["db:migrate"] = "pgstrap migrate" pkg.scripts["db:reset"] = "pgstrap reset" - pkg.scripts["db:generate"] = "pgstrap generate" + pkg.scripts["db:generate"] = "pgstrap generate --pglite" pkg.scripts["db:create-migration"] = "pgstrap create-migration" if (!pkg.devDependencies) pkg.devDependencies = {} diff --git a/tests/generate.pglite.test.ts b/tests/generate.pglite.test.ts index 56dcd53..fc67661 100644 --- a/tests/generate.pglite.test.ts +++ b/tests/generate.pglite.test.ts @@ -13,23 +13,29 @@ exports.down = async (pgm) => { } ` -test("generate with pglite runs migrations and dumps structure", async () => { +const makeProject = (): { + tmp: string + migrationsDir: string + dbDir: string +} => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pgstrap-generate-")) const migrationsDir = path.join(tmp, "migrations") + const dbDir = path.join(tmp, "db") + 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"), + return { + tmp, migrationsDir, - pglite: true, - }) + dbDir, + } +} +const expectGeneratedSchema = (tmp: string): void => { const zapatosFile = path.join(tmp, "db", "zapatos", "schema.d.ts") const structureDir = path.join( tmp, @@ -42,6 +48,39 @@ test("generate with pglite runs migrations and dumps structure", async () => { expect(fs.existsSync(zapatosFile)).toBe(true) expect(fs.existsSync(path.join(structureDir, "table.sql"))).toBe(true) +} + +test("generate defaults to pglite and dumps structure without a running postgres server", async () => { + const { tmp, migrationsDir, dbDir } = makeProject() + + try { + await generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir, + migrationsDir, + }) + + expectGeneratedSchema(tmp) + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + +test("generate with pglite runs migrations and dumps structure", async () => { + const { tmp, migrationsDir, dbDir } = makeProject() + + try { + await generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir, + migrationsDir, + pglite: true, + }) - fs.rmSync(tmp, { recursive: true, force: true }) + expectGeneratedSchema(tmp) + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } }) diff --git a/tests/init.test.ts b/tests/init.test.ts index cd4ec4b..aabc7f7 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -25,6 +25,6 @@ test("initPgstrap writes scripts to package.json", async () => { ) expect(pkg.scripts["db:migrate"]).toBe("pgstrap migrate") expect(pkg.scripts["db:reset"]).toBe("pgstrap reset") - expect(pkg.scripts["db:generate"]).toBe("pgstrap generate") + expect(pkg.scripts["db:generate"]).toBe("pgstrap generate --pglite") expect(pkg.scripts["db:create-migration"]).toBe("pgstrap create-migration") })