Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ npm install pgstrap --save-dev
npm run db:migrate
```

6. Generate types and structure:
6. Generate types and structure (no running Postgres server required — this
uses an in-memory PGlite database by default):
```bash
npm run db:generate
```
Expand All @@ -55,7 +56,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. By default this runs your migrations against an in-memory [PGlite](https://pglite.dev/) instance, so no running Postgres server is required. Use `pgstrap generate --no-pglite` to connect to a real Postgres database (via `DATABASE_URL`) instead.
- `npm run db:create-migration` - Create a new migration file

### Configuration
Expand Down
15 changes: 12 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env node
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { migrate, reset, generate, createMigration, initPgstrap } from "./"
import { getProjectContext } from "./get-project-context"
;(yargs as any)
;(yargs as any)(hideBin(process.argv))
.command("init", "initialize pgstrap", {}, async () => {
await initPgstrap({
cwd: process.cwd(),
Expand Down Expand Up @@ -35,10 +36,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 against an in-memory PGlite database, so no running Postgres server is required. Use --no-pglite to connect to a real Postgres database instead.",
})
},
async (argv) => {
generate({ ...(await getProjectContext()), pglite: !!argv.pglite })
await generate({
...(await getProjectContext()),
pglite: argv.pglite !== false,
})
},
)
.parse()
44 changes: 25 additions & 19 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export const generate = async ({
schemas,
defaultDatabase,
dbDir,
pglite = false,
// Defaults to an in-memory PGlite database so that a running Postgres
// server isn't required to generate types. Pass `pglite: false` to
// connect to a real Postgres database instead.
pglite = true,
migrationsDir,
}: Pick<Context, "schemas" | "defaultDatabase" | "dbDir"> & {
pglite?: boolean
Expand Down Expand Up @@ -67,25 +70,28 @@ export const generate = async ({
const prevDbUrl = process.env.DATABASE_URL
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,
})
try {
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 {
server.close()
await db.close().catch(() => {})
if (prevDbUrl === undefined) delete process.env.DATABASE_URL
else process.env.DATABASE_URL = prevDbUrl
}
return
}

Expand Down
5 changes: 4 additions & 1 deletion src/get-project-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "fs"
import path from "path"
import { pathToFileURL } from "url"
import { PgstrapConfig } from "./define-config"

export interface Context extends PgstrapConfig {
Expand All @@ -13,7 +14,9 @@ export const getProjectContext = async (): Promise<Context> => {
)
}

const config = await import(path.join(process.cwd(), "pgstrap.config.js"))
const config = await import(
pathToFileURL(path.join(process.cwd(), "pgstrap.config.js")).href
)

return {
cwd: process.cwd(),
Expand Down
78 changes: 78 additions & 0 deletions tests/cli-generate-default.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { test, expect } from "bun:test"
import { spawnSync } from "child_process"
import fs from "fs"
import os from "os"
import path from "path"

const migrationFile = `
exports.up = async (pgm) => {
pgm.createTable('widgets', { id: 'id', name: { type: 'text' } })
}
exports.down = async (pgm) => {
pgm.dropTable('widgets')
}
`

// Regression test for https://github.com/seveibar/pgstrap/issues/2
// Running "pgstrap generate" (i.e. the scaffolded "db:generate" script)
// with no flags must succeed without a running Postgres server, because
// it defaults to an in-memory PGlite database.
test(
"pgstrap generate defaults to pglite and works without postgres",
async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pgstrap-cli-generate-"))
try {
fs.writeFileSync(
path.join(tmp, "pgstrap.config.js"),
`module.exports = ${JSON.stringify({
defaultDatabase: "some_db_that_does_not_exist",
schemas: ["public"],
})}`,
)
const migrationsDir = path.join(tmp, "src", "db", "migrations")
fs.mkdirSync(migrationsDir, { recursive: true })
fs.writeFileSync(
path.join(migrationsDir, "001_create_widgets.js"),
migrationFile,
)

const env = { ...process.env }
// Make sure no real postgres connection info leaks into the test
delete env.DATABASE_URL
delete env.PGHOST
delete env.PGPORT
delete env.PGUSER
delete env.PGPASSWORD
delete env.PGDATABASE
// Point at a port where nothing is listening so a fallback to a real
// postgres connection would fail loudly
env.PGPORT = "54329"

const result = spawnSync(
process.execPath,
[path.join(__dirname, "..", "src", "cli.ts"), "generate"],
{ cwd: tmp, env, encoding: "utf8" },
)

expect(result.status).toBe(0)

const zapatosFile = path.join(tmp, "src", "db", "zapatos", "schema.d.ts")
const structureFile = path.join(
tmp,
"src",
"db",
"structure",
"public",
"tables",
"widgets",
"table.sql",
)
expect(fs.existsSync(zapatosFile)).toBe(true)
expect(fs.existsSync(structureFile)).toBe(true)
expect(fs.readFileSync(zapatosFile, "utf8")).toContain("widgets")
} finally {
fs.rmSync(tmp, { recursive: true, force: true })
}
},
60_000,
)