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
141 changes: 2 additions & 139 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,145 +1,8 @@
# pgstrap

[![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)
PostgreSQL type generation that **doesn't require a running PostgreSQL instance**.

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!

## Features

- Migrations with TypeScript support
- Organized migration structure in `[src]/db/migrations`
- Database reset and migration scripts
- Automatic typed schema files in `[src]/db/zapatos` (compatible with Zapatos and Kysely)
- Database `table.sql` structure dumps in `[src]/db/structure`
- Easily run migrations inside of tests
pgstrap uses [PGlite](https://github.com/electric-sql/pglite) — a WebAssembly build of PostgreSQL — to run your migrations in-memory and generate TypeScript types from your schema, with zero external dependencies.

## Installation

```bash
npm install pgstrap --save-dev
```

## Quick Start

1. Initialize pgstrap in your project:

```bash
npx pgstrap init
```

2. This will set up the necessary configuration and scripts in your `package.json`.

3. Create your first migration:

```bash
npm run db:create-migration my-first-migration
```

4. Edit the migration file in `src/db/migrations/`.

5. Run the migration:

```bash
npm run db:migrate
```

6. Generate types and structure:
```bash
npm run db:generate
```

## Usage

### Available Scripts

- `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:create-migration` - Create a new migration file

### Configuration

After running `pgstrap init`, you'll find a `pgstrap.config.js` file in your project root. Customize it as needed:

```javascript
module.exports = {
defaultDatabase: "mydb",
schemas: ["public"],
dbDir: "./src/db", // optional, defaults to "./src/db"
}
```

## Writing Migrations

Migrations in pgstrap are written using [node-pg-migrate](https://github.com/salsita/node-pg-migrate), which provides a powerful and flexible way to define database changes.

To create a new migration:

```bash
npm run db:create-migration my-migration-name
```

This will create a new file in `src/db/migrations/` with a timestamp prefix.

### Migration Structure

A typical migration file looks like this:

```typescript
import { MigrationBuilder, ColumnDefinitions } from "node-pg-migrate"

export const shorthands: ColumnDefinitions | undefined = undefined

export async function up(pgm: MigrationBuilder): Promise<void> {
// Your migration code here
}

export async function down(pgm: MigrationBuilder): Promise<void> {
// Code to revert your migration (optional)
}
```

### Example Migration

Here's an example of a migration that creates a new table:

```typescript
export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.createTable("users", {
id: "id",
username: { type: "text", notNull: true },
email: { type: "text", notNull: true, unique: true },
created_at: {
type: "timestamptz",
notNull: true,
default: pgm.func("current_timestamp"),
},
})

pgm.createIndex("users", "username")
}

export async function down(pgm: MigrationBuilder): Promise<void> {
pgm.dropTable("users")
}
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

- This project was originally developed as [seam-pgm](https://github.com/seamapi/seam-pgm) by Seam Labs Inc. in 2024.
- Special thanks to the Zapatos and Kysely projects for their excellent TypeScript database tools.

## Support

If you encounter any issues or have questions, please file an issue on the [GitHub repository](https://github.com/seveibar/pgstrap/issues).

Happy coding with pgstrap! 🚀
54 changes: 13 additions & 41 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,52 +1,24 @@
{
"name": "pgstrap",
"repository": "seveibar/pgstrap",
"type": "module",
"version": "1.0.6",
"description": "Bootstrap patterns for Typescript/Postgres projects",
"main": "dist/index.cjs",
"scripts": {
"test": "bun test",
"build": "tsup ./src/index.ts ./src/cli.ts --outDir ./dist --dts --sourcemap inline",
"format": "biome format . --write",
"format:check": "biome format ."
},
"version": "0.1.0",
"description": "PostgreSQL type generation tool that doesn't require a running postgres instance",
"main": "dist/index.js",
"bin": {
"pgstrap": "./dist/cli.cjs"
"pgstrap": "dist/cli.js"
},
"scripts": {
"build": "bun build src/cli.ts --outdir dist --target node",
"test": "bun test"
},
"keywords": [
"postgres",
"typescript",
"bootstrap"
],
"author": "",
"license": "MIT",
"dependencies": {
"@electric-sql/pglite": "^0.3.3",
"pg-gateway": "^0.3.0-beta.4",
"pg-schema-dump": "^2.0.2",
"@electric-sql/pglite": "^0.2.0",
"glob": "^10.3.10",
"pg": "^8.11.3",
"yargs": "^17.7.2"
},
"peerDependencies": {
"esbuild-register": "*",
"kysely": "*",
"node-pg-migrate": "*",
"pg-connection-from-env": "*",
"zapatos": "*"
},
"devDependencies": {
"@biomejs/biome": "^1.5.3",
"@types/debug": "^4.1.8",
"@types/pg": "^8.11.0",
"@types/yargs": "^17.0.32",
"esbuild": "^0.18.16",
"esbuild-register": "^3.4.2",
"esbuild-runner": "^2.2.2",
"mkdirp": "^3.0.1",
"mock-fs": "^5.2.0",
"node-pg-migrate": "^6.2.2",
"pg": "^8.11.1",
"pg-connection-from-env": "^1.1.0",
"tsup": "^7.1.0",
"zapatos": "^6.1.4"
"typescript": "^5.3.3"
}
}
174 changes: 131 additions & 43 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,132 @@
#!/usr/bin/env node
import yargs from "yargs"
import { migrate, reset, generate, createMigration, initPgstrap } from "./"
import { getProjectContext } from "./get-project-context"
;(yargs as any)
.command("init", "initialize pgstrap", {}, async () => {
await initPgstrap({
cwd: process.cwd(),
})
})
.command(
"create-migration [name]",
"create a new migration",
(yargs) => {
yargs.positional("name", {
describe: "name of the migration file",
type: "string",
})
},
async (argv) => {
const ctx = await getProjectContext()
createMigration(argv.name as string, ctx)
},
)
.command("reset", "resets the database", {}, async () => {
await reset(await getProjectContext())

// Reset hangs, probably due to unclosed pg connection
process.exit(0)
})
.command("migrate", "migrates the database", {}, async () => {
migrate(await getProjectContext())
})
.command(
"generate",
"generate types and sql documentation from database",
(yargs) => {
yargs.option("pglite", { type: "boolean", default: false })
},
async (argv) => {
generate({ ...(await getProjectContext()), pglite: !!argv.pglite })
},
)
.parse()
import * as path from "path";
import * as fs from "fs";
import { generate } from "./generate";

interface Config {
migrationsDir?: string;
schemaFile?: string;
outputFile?: string;
databaseUrl?: string;
}

function loadConfig(): Config {
const configPaths = [
"pgstrap.config.json",
"pgstrap.config.js",
"pgstrap.config.ts",
];

for (const configPath of configPaths) {
if (fs.existsSync(configPath)) {
try {
if (configPath.endsWith(".json")) {
const content = fs.readFileSync(configPath, "utf-8");
return JSON.parse(content);
}
// For .js/.ts configs, try require
return require(path.resolve(configPath));
} catch (err) {
// ignore
}
}
}

return {};
}

async function main() {
const args = process.argv.slice(2);
const command = args[0];

if (!command || command === "help" || command === "--help" || command === "-h") {
console.log(`
pgstrap - PostgreSQL type generation (no running PostgreSQL required!)

Usage:
pgstrap generate [options]
pgstrap gen [options]

Options:
--migrations-dir <dir> Directory containing SQL migration files (default: ./migrations)
--schema-file <file> Single SQL schema file to use
--output <file> Output file for generated types (default: ./src/db/schema.ts)
--database-url <url> PostgreSQL connection URL (optional, uses PGlite if not provided)
--help Show this help message

Configuration:
You can also use a pgstrap.config.json file:
{
"migrationsDir": "./migrations",
"outputFile": "./src/db/schema.ts"
}

Examples:
pgstrap generate
pgstrap generate --migrations-dir ./db/migrations --output ./src/types/db.ts
pgstrap generate --schema-file ./schema.sql
`);
process.exit(0);
}

if (command === "generate" || command === "gen") {
const config = loadConfig();

// Parse CLI arguments
let migrationsDir = config.migrationsDir;
let schemaFile = config.schemaFile;
let outputFile = config.outputFile || "./src/db/schema.ts";
let databaseUrl = config.databaseUrl || process.env.DATABASE_URL;

for (let i = 1; i < args.length; i++) {
if (args[i] === "--migrations-dir" && args[i + 1]) {
migrationsDir = args[++i];
} else if (args[i] === "--schema-file" && args[i + 1]) {
schemaFile = args[++i];
} else if (args[i] === "--output" && args[i + 1]) {
outputFile = args[++i];
} else if (args[i] === "--database-url" && args[i + 1]) {
databaseUrl = args[++i];
}
}

// Default migrations dir
if (!migrationsDir && !schemaFile) {
const defaultDirs = ["./migrations", "./db/migrations", "./database/migrations", "./drizzle"];
for (const dir of defaultDirs) {
if (fs.existsSync(dir)) {
migrationsDir = dir;
break;
}
}
}

if (migrationsDir) {
console.log(`Using migrations directory: ${migrationsDir}`);
}
if (schemaFile) {
console.log(`Using schema file: ${schemaFile}`);
}

try {
await generate({
migrationsDir,
schemaFile,
outputFile,
databaseUrl,
});
} catch (err: any) {
console.error("Error generating types:", err.message);
process.exit(1);
}
} else {
console.error(`Unknown command: ${command}`);
console.error("Run 'pgstrap --help' for usage information.");
process.exit(1);
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading