@darkj/create-db 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # create-db
2
+
3
+ CLI to scaffold PostgreSQL database projects with a modular, maintainable structure.
4
+
5
+ Generates a project with two independent layers:
6
+ - **`schema/`** — current design: table definitions organized by domain
7
+ - **`migrations/`** — history: incremental changes applied to existing databases
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ npx create-db
13
+ ```
14
+
15
+ Prompts for a project name and whether you'll use Claude Code, then scaffolds the project and installs dependencies.
16
+
17
+ ## What gets generated
18
+
19
+ ```
20
+ my-project-db/
21
+ ├── init.sql ← DROP ALL + reload schema (for fresh DBs)
22
+ ├── package.json ← npm run build / migrate / rollback
23
+ ├── tsconfig.json
24
+ ├── .env.example ← copy to .env and fill credentials
25
+ ├── scripts/
26
+ │ ├── build.ts ← merges schema/ into db-output.sql
27
+ │ ├── migrate.ts ← applies pending migrations to an existing DB
28
+ │ └── rollback.ts ← reverts last N applied migrations
29
+ ├── schema/
30
+ │ └── init.sql ← define your tables here
31
+ ├── migrations/ ← one folder per change (up.sql + down.sql)
32
+ └── backups/ ← pg_dump outputs go here (gitignored)
33
+ ```
34
+
35
+ ## Available commands in the generated project
36
+
37
+ ```bash
38
+ npm run build # merge schema/ into db-output.sql (no DB needed)
39
+ npm run migrate # apply pending migrations to an existing DB
40
+ npm run rollback # revert last migration
41
+ npm run rollback 3 # revert last 3 migrations
42
+ ```
43
+
44
+ ## Two workflows
45
+
46
+ **Fresh database (dev, CI, new environment)**
47
+ ```bash
48
+ psql -U user -d mydb -f init.sql
49
+ ```
50
+
51
+ **Existing database with data**
52
+ ```bash
53
+ npm run migrate
54
+ ```
55
+
56
+ ## Requirements
57
+
58
+ - Node.js >= 18
59
+ - PostgreSQL
60
+
61
+ ## Getting started
62
+
63
+ ```bash
64
+ npx create-db
65
+
66
+ cd my-project-db
67
+ cp .env.example .env # fill in DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
68
+ npm run build # test with no DB needed
69
+ psql -U user -d mydb -f init.sql # initialize a fresh DB
70
+ ```
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateProject = generateProject;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
40
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
41
+ function copyDir(src, dest, vars = {}, skip = []) {
42
+ fs.mkdirSync(dest, { recursive: true });
43
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
44
+ if (skip.includes(entry.name))
45
+ continue;
46
+ const srcPath = path.join(src, entry.name);
47
+ const destPath = path.join(dest, entry.name);
48
+ if (entry.isDirectory()) {
49
+ copyDir(srcPath, destPath, vars, skip);
50
+ }
51
+ else {
52
+ let content = fs.readFileSync(srcPath, 'utf-8');
53
+ for (const [key, val] of Object.entries(vars)) {
54
+ content = content.replaceAll(`{{${key}}}`, val);
55
+ }
56
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
57
+ fs.writeFileSync(destPath, content);
58
+ }
59
+ }
60
+ }
61
+ async function generateProject({ projectName, targetDir, withClaude }) {
62
+ if (fs.existsSync(targetDir)) {
63
+ throw new Error(`Directory "${projectName}" already exists.`);
64
+ }
65
+ // 1. Copy template files (skip .claude if not needed)
66
+ const skip = withClaude ? [] : ['.claude'];
67
+ copyDir(path.join(TEMPLATES_DIR, 'base'), targetDir, { PROJECT_NAME: projectName }, skip);
68
+ console.log(` ✓ Template files copied${withClaude ? ' (with .claude)' : ''}`);
69
+ // 2. Generate package.json with the project name and scripts (no pinned versions)
70
+ const pkg = {
71
+ name: projectName,
72
+ version: '1.0.0',
73
+ scripts: {
74
+ build: 'tsx scripts/build.ts',
75
+ migrate: 'tsx scripts/migrate.ts',
76
+ rollback: 'tsx scripts/rollback.ts',
77
+ },
78
+ };
79
+ fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
80
+ console.log(' ✓ package.json generated');
81
+ // 3. Install dependencies (latest versions at scaffold time)
82
+ console.log(' Installing dependencies...');
83
+ (0, child_process_1.execSync)('npm install pg', { cwd: targetDir, stdio: 'inherit' });
84
+ (0, child_process_1.execSync)('npm install -D tsx typescript @types/node @types/pg', { cwd: targetDir, stdio: 'inherit' });
85
+ console.log(' ✓ Dependencies installed');
86
+ }
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const p = __importStar(require("@clack/prompts"));
38
+ const path = __importStar(require("path"));
39
+ const generator_1 = require("./generator");
40
+ async function main() {
41
+ console.log();
42
+ p.intro('create-db');
43
+ const projectName = await p.text({
44
+ message: 'Project name',
45
+ placeholder: 'my-project-db',
46
+ validate: (v) => !v.trim() ? 'Project name is required.' : undefined,
47
+ });
48
+ if (p.isCancel(projectName)) {
49
+ p.cancel('Cancelled.');
50
+ process.exit(0);
51
+ }
52
+ const useClaude = await p.confirm({
53
+ message: 'Will you use Claude Code in this project?',
54
+ initialValue: true,
55
+ });
56
+ if (p.isCancel(useClaude)) {
57
+ p.cancel('Cancelled.');
58
+ process.exit(0);
59
+ }
60
+ const name = projectName;
61
+ const targetDir = path.join(process.cwd(), name);
62
+ p.log.step('Scaffolding database project...');
63
+ console.log();
64
+ try {
65
+ await (0, generator_1.generateProject)({ projectName: name, targetDir, withClaude: useClaude });
66
+ }
67
+ catch (err) {
68
+ p.log.error(String(err));
69
+ process.exit(1);
70
+ }
71
+ console.log();
72
+ p.outro(`Done! Next steps:\n\n` +
73
+ ` cd ${name}\n` +
74
+ ` cp .env.example .env\n` +
75
+ ` # Fill in your DB credentials\n` +
76
+ ` npm run build`);
77
+ }
78
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@darkj/create-db",
3
+ "version": "1.0.0",
4
+ "description": "CLI to scaffold PostgreSQL database projects with a modular, maintainable structure",
5
+ "bin": {
6
+ "create-db": "./dist/index.js"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepare": "npm run build",
14
+ "dev": "ts-node src/index.ts",
15
+ "start": "node dist/index.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "templates"
23
+ ],
24
+ "dependencies": {
25
+ "@clack/prompts": "^0.7.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.11.5",
29
+ "ts-node": "^10.9.2",
30
+ "typescript": "^5.3.3"
31
+ }
32
+ }
@@ -0,0 +1,83 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Base de datos PostgreSQL — generada con `create-db`.
4
+ Mi rol aquí es asistir en el diseño, organización, análisis y evolución de esta DB.
5
+
6
+ ---
7
+
8
+ ## Cómo funciona
9
+
10
+ ### Dos capas independientes
11
+
12
+ ```
13
+ schema/ → diseño actual: qué tablas existen y cómo están definidas
14
+ migrations/ → historia: qué cambió, en qué orden, ya aplicado a DBs reales
15
+ ```
16
+
17
+ Editar `schema/` no afecta una DB existente. Para cambiar una DB con datos, siempre hay que crear una migración.
18
+
19
+ ### Dos flujos
20
+
21
+ | Situación | Comando |
22
+ |---|---|
23
+ | DB vacía (dev, CI, nuevo entorno) | `psql -U user -d dbname -f init.sql` |
24
+ | DB existente con datos reales | `npm run migrate` |
25
+
26
+ ### Scripts
27
+
28
+ | Comando | Hace | Necesita DB |
29
+ |---|---|---|
30
+ | `npm run build` | Fusiona todo `schema/` en `db-output.sql` siguiendo los `\i` recursivamente | No |
31
+ | `init.sql` | DROP de todo + carga `schema/` desde cero | Sí |
32
+ | `npm run migrate` | Aplica solo las migraciones de `migrations/` aún no registradas en `_migrations` | Sí |
33
+ | `npm run rollback [N]` | Revierte las últimas N migraciones ejecutando su `down.sql` (default: 1) | Sí |
34
+
35
+ ### Estructura del schema
36
+
37
+ ```
38
+ schema/
39
+ ├── init.sql ← orden global (dependencias primero)
40
+ ├── tabla-standalone/ ← tabla transversal a varios dominios
41
+ │ ├── create.sql
42
+ │ ├── data.sql
43
+ │ ├── init.sql
44
+ │ └── DICTIONARY.md
45
+ └── nombre-grupo/ ← dominio lógico (auth, orders, catalog…)
46
+ ├── init.sql ← orden dentro del grupo
47
+ └── tabla/
48
+ ├── create.sql
49
+ ├── data.sql
50
+ ├── triggers.sql ← solo si aplica
51
+ ├── init.sql
52
+ └── DICTIONARY.md
53
+ ```
54
+
55
+ El orden de dependencias es **siempre manual**. Quien referencia va después del referenciado.
56
+
57
+ ---
58
+
59
+ ## Comandos disponibles
60
+
61
+ - `/analyze` — análisis rápido de la DB actual (estructura, relaciones, catálogos, triggers)
62
+ - `/new-migration` — guía interactiva para crear una migración correctamente
63
+ - `/new-dictionary` — genera el `DICTIONARY.md` de una tabla a partir de su `create.sql`
64
+ - `/build` — corre `npm run build` y explica el `db-output.sql` generado
65
+ - `/backup` — crea un dump de la DB con nombre y ubicación correctos
66
+
67
+ ---
68
+
69
+ ## Backups
70
+
71
+ Los dumps van en `backups/` con formato: `YYYYMMDD_HHMMSS_descripcion.sql`
72
+
73
+ ```bash
74
+ pg_dump -U user -d dbname -F p -f backups/$(date +%Y%m%d_%H%M%S)_descripcion.sql
75
+ ```
76
+
77
+ Los archivos `.sql` dentro de `backups/` están en `.gitignore` — no se suben al repo.
78
+
79
+ ---
80
+
81
+ ## Learnings de este proyecto
82
+
83
+ <!-- Aquí se van agregando decisiones, patrones y convenciones específicas de esta DB -->
@@ -0,0 +1,18 @@
1
+ Analiza la base de datos de este proyecto siguiendo estos pasos en orden:
2
+
3
+ 1. Lee `schema/init.sql` para ver el orden global: qué grupos y tablas standalone existen
4
+ 2. Por cada grupo, lee su `init.sql` para ver las tablas que contiene y su orden de dependencia
5
+ 3. Por cada tabla, lee su `create.sql` para ver estructura de columnas, tipos, constraints y FKs
6
+ 4. Lee cada `data.sql` para identificar qué datos son de catálogo/configuración inicial
7
+ 5. Lee cada `triggers.sql` donde exista para entender lógica automática en la DB
8
+ 6. Lee `migrations/` en orden para ver la historia de cambios desde el baseline
9
+
10
+ Con todo eso, produce un reporte estructurado que incluya:
11
+
12
+ - **Resumen**: qué hace esta base de datos, en una oración
13
+ - **Grupos y tablas**: lista jerárquica de grupos → tablas con descripción de cada una
14
+ - **Relaciones clave**: las FKs más importantes y qué representan
15
+ - **Datos de catálogo**: qué tablas tienen datos iniciales y qué contienen
16
+ - **Lógica en DB**: triggers existentes y qué hacen
17
+ - **Historia de cambios**: qué migraciones hay y qué cambió en cada una
18
+ - **Observaciones**: algo que llame la atención (diseño, dependencias, datos faltantes, etc.)
@@ -0,0 +1,19 @@
1
+ Creá un backup de la base de datos en el directorio `backups/`.
2
+
3
+ Primero leé `.env` para obtener las credenciales (DB_HOST, DB_PORT, DB_USER, DB_NAME).
4
+
5
+ Preguntame una descripción breve del backup (ej: "before_migration_005", "pre_launch", "post_seed").
6
+
7
+ Luego mostrá el comando exacto a correr:
8
+
9
+ ```bash
10
+ pg_dump -U {DB_USER} -h {DB_HOST} -p {DB_PORT} -d {DB_NAME} -F p \
11
+ -f backups/$(date +%Y%m%d_%H%M%S)_{descripcion}.sql
12
+ ```
13
+
14
+ El formato del nombre es: `YYYYMMDD_HHMMSS_descripcion.sql`
15
+
16
+ Recordá que los `.sql` dentro de `backups/` están en `.gitignore` — no se suben al repo.
17
+ El directorio `backups/` sí está trackeado (gracias al `.gitkeep`).
18
+
19
+ Después de correr el comando, verificá que el archivo existe y mostrá su tamaño.
@@ -0,0 +1,17 @@
1
+ Ejecutá el script de build desde la raíz del proyecto:
2
+
3
+ ```bash
4
+ npm run build
5
+ ```
6
+
7
+ Luego leé el archivo `db-output.sql` generado y reportá:
8
+ - Cuántas tablas/grupos fueron incluidos
9
+ - El orden final de carga (útil para detectar errores de dependencia)
10
+ - Si hay alguna advertencia de `[WARN] Not found` en la salida del script
11
+
12
+ Si el usuario quiere usar el output para inicializar una DB:
13
+ ```bash
14
+ psql -U user -d dbname -f db-output.sql
15
+ ```
16
+
17
+ Recordá que `db-output.sql` está en `.gitignore` — es un archivo generado, no se sube al repo.
@@ -0,0 +1,44 @@
1
+ Genera el archivo DICTIONARY.md para una tabla de este proyecto.
2
+
3
+ Si el usuario no especificó qué tabla, preguntá cuál.
4
+
5
+ Luego:
6
+ 1. Leé el `create.sql` de esa tabla para obtener columnas, tipos, constraints y FKs
7
+ 2. Leé el `data.sql` para entender qué datos iniciales maneja (útil para las reglas de negocio)
8
+ 3. Si existe `triggers.sql`, leélo para documentarlo en Notas técnicas
9
+
10
+ Generá el `DICTIONARY.md` siguiendo exactamente esta plantilla:
11
+
12
+ ---
13
+
14
+ # {nombre_tabla}
15
+
16
+ {Descripción en 1-2 líneas: qué representa esta tabla en el dominio del negocio.}
17
+
18
+ ## Columnas
19
+
20
+ | Columna | Tipo | Nulo | Default | Descripción |
21
+ |---------|------|------|---------|-------------|
22
+ | {col} | {tipo} | NO/SÍ | {default o —} | {descripción en lenguaje de negocio} |
23
+
24
+ ## Relaciones
25
+
26
+ | Columna | Referencia | Descripción |
27
+ |---------|------------|-------------|
28
+ | {fk_col} | {tabla.col} | {qué representa esta relación en el negocio} |
29
+
30
+ ## Reglas de negocio
31
+
32
+ - {regla o restricción relevante para el negocio}
33
+
34
+ ## Notas técnicas
35
+
36
+ - {constraints, triggers, índices relevantes que no son obvios del create.sql}
37
+
38
+ ---
39
+
40
+ Reglas al generar:
41
+ - Descripciones siempre en lenguaje de negocio, no técnico
42
+ - Si una sección no aplica (sin FKs, sin reglas especiales), omitirla completamente
43
+ - El orden de filas en Columnas debe seguir el mismo orden que en el create.sql
44
+ - Guardarlo en: schema/{grupo-si-aplica}/{tabla}/DICTIONARY.md
@@ -0,0 +1,61 @@
1
+ Creá una nueva migración para este proyecto siguiendo el formato de carpetas.
2
+
3
+ ## Paso 1 — Determinar el número siguiente
4
+
5
+ Listá las carpetas en `migrations/` en orden alfabético.
6
+ El próximo número es el último + 1, con ceros a la izquierda (ej: si existe `004_...`, el siguiente es `005`).
7
+
8
+ ## Paso 2 — Obtener información
9
+
10
+ Si el usuario no la dio, preguntá:
11
+ - ¿Qué cambio se quiere hacer?
12
+ - ¿Hay contexto o razón detrás del cambio? (para decidir si crear README.md)
13
+
14
+ Si el usuario pasó un script SQL directamente, usarlo como base para `up.sql` — ordenarlo y limpiarlo si hace falta.
15
+
16
+ ## Paso 3 — Crear la carpeta
17
+
18
+ Nombre: `NNN_YYYYMMDD_HHMM_proposito_breve`
19
+ - Fecha y hora actuales
20
+ - Propósito en snake_case, breve (3-5 palabras máximo)
21
+
22
+ ```
23
+ migrations/NNN_YYYYMMDD_HHMM_proposito_breve/
24
+ ├── up.sql
25
+ ├── down.sql
26
+ └── README.md ← solo si hay contexto claro
27
+ ```
28
+
29
+ ## Paso 4 — Escribir up.sql
30
+
31
+ Aplicar el checklist:
32
+ - ¿Columna NOT NULL nueva? → necesita DEFAULT o backfill previo
33
+ - ¿Eliminás columna con datos? → primero migrar datos, luego DROP
34
+ - ¿FK nueva? → la tabla referenciada ya debe existir
35
+ - ¿Índice en tabla grande? → sugerir CONCURRENTLY
36
+
37
+ ## Paso 5 — Escribir down.sql
38
+
39
+ SQL inverso al up.sql. Debe dejar la DB en el estado anterior exactamente.
40
+ Siempre escribirlo, aunque el usuario no lo haya pedido.
41
+
42
+ ## Paso 6 — Crear README.md (solo si hay contexto)
43
+
44
+ Si la migración fue planeada o el usuario explicó la razón:
45
+ ```markdown
46
+ # proposito_breve
47
+
48
+ {1-3 líneas del por qué, no del qué.}
49
+ ```
50
+
51
+ Si el usuario pasó el script sin contexto: no crear README.md.
52
+
53
+ ## Paso 7 — Actualizar schema/
54
+
55
+ Identificá qué archivos `create.sql` en `schema/` quedan desactualizados y actualizalos para reflejar el nuevo estado de las tablas afectadas.
56
+
57
+ ## Paso 8 — Mostrar el comando para aplicar
58
+
59
+ ```bash
60
+ npm run migrate
61
+ ```
@@ -0,0 +1,111 @@
1
+ ---
2
+ paths:
3
+ - "migrations/**"
4
+ ---
5
+
6
+ # Reglas para trabajar en migrations/
7
+
8
+ ## Principio fundamental
9
+
10
+ `migrations/` es **append-only** e **inmutable**. Solo se agregan carpetas nuevas al final.
11
+ Nunca editar `up.sql` de una migración ya aplicada — si hay un error, crear una nueva migración que lo corrija.
12
+
13
+ ## Estructura de cada migración
14
+
15
+ Cada migración es una **carpeta**, no un archivo suelto:
16
+
17
+ ```
18
+ migrations/
19
+ └── NNN_YYYYMMDD_HHMM_proposito_breve/
20
+ ├── up.sql ← cambios a aplicar (obligatorio)
21
+ ├── down.sql ← cómo revertirlos (obligatorio)
22
+ └── README.md ← contexto y razón (solo si fue planeado o tiene contexto claro)
23
+ ```
24
+
25
+ ## Naming de la carpeta
26
+
27
+ ```
28
+ NNN_YYYYMMDD_HHMM_proposito_breve
29
+ ```
30
+
31
+ - `NNN` — número secuencial con ceros (`001`, `002`, `003`...)
32
+ - `YYYYMMDD` — fecha de creación
33
+ - `HHMM` — hora de creación
34
+ - `proposito_breve` — descripción corta en `snake_case`
35
+
36
+ Ejemplos:
37
+ - `003_20260524_1430_add_phone_to_users`
38
+ - `004_20260525_0900_create_index_orders_status`
39
+ - `005_20260526_1100_rename_column_amount_to_total`
40
+ - `006_20260527_1600_drop_deprecated_sessions`
41
+
42
+ ## up.sql — los cambios
43
+
44
+ SQL que transforma la DB del estado anterior al nuevo. Es lo que `npm run migrate` ejecuta.
45
+
46
+ ## down.sql — el reverso
47
+
48
+ SQL inverso al `up.sql`. Debe dejar la DB exactamente como estaba antes de aplicar esta migración.
49
+ Siempre escribirlo aunque no se planee usarlo — es la documentación del efecto contrario.
50
+
51
+ ## README.md — contexto (opcional)
52
+
53
+ Solo crearlo si la migración fue **planeada** o tiene **contexto conocido** (decisión de negocio, bug fix, refactor coordinado).
54
+ Si el usuario pasa un script sin contexto, generar solo `up.sql` y `down.sql`, sin `README.md`.
55
+
56
+ Formato del README.md:
57
+ ```markdown
58
+ # proposito_breve
59
+
60
+ {1-3 líneas explicando por qué se hace este cambio, no qué hace el SQL.}
61
+ ```
62
+
63
+ ## Checklist antes de escribir up.sql
64
+
65
+ 1. ¿El cambio afecta datos existentes? → incluir `UPDATE`/backfill antes del `ALTER`
66
+ 2. ¿Se agrega columna `NOT NULL`? → necesita `DEFAULT` o backfill previo
67
+ 3. ¿Se elimina columna con datos? → primero migrar los datos, luego `DROP`
68
+ 4. ¿Hay FK nueva? → la tabla referenciada ya debe existir
69
+ 5. ¿Se crea índice en tabla grande? → considerar `CREATE INDEX CONCURRENTLY`
70
+ 6. ¿El SQL es idempotente donde sea posible? → usar `IF NOT EXISTS`, `IF EXISTS`, `CREATE OR REPLACE`
71
+
72
+ ## Cómo aplica npm run migrate
73
+
74
+ 1. Conecta a la DB leyendo `.env`
75
+ 2. Crea tabla `_migrations` si no existe
76
+ 3. Lista las **carpetas** de `migrations/` en orden alfabético
77
+ 4. Ejecuta `up.sql` de las carpetas no registradas en `_migrations`
78
+ 5. Registra el **nombre de la carpeta** con timestamp al aplicarla
79
+ 6. Cada migración se ejecuta en una transacción — si falla, hace ROLLBACK y para
80
+
81
+ ## También actualizar schema/
82
+
83
+ Al crear una migración, actualizar también el `create.sql` correspondiente en `schema/` para que refleje el estado actual. `schema/` es el diseño vigente; `migrations/` es cómo llegamos ahí.
84
+
85
+ ## Cómo revertir migraciones (npm run rollback)
86
+
87
+ `rollback.ts` elimina el registro de `_migrations` y luego ejecuta el `down.sql` dentro de la misma transacción — en ese orden, para que si el `down.sql` dropa la tabla `_migrations`, la transacción igual cierre limpia.
88
+
89
+ ```bash
90
+ # Revertir la última migración aplicada
91
+ npm run rollback
92
+
93
+ # Revertir las últimas N migraciones (en orden inverso)
94
+ npm run rollback 3
95
+ ```
96
+
97
+ Cómo funciona:
98
+ 1. Verifica que la tabla `_migrations` exista — si no, no hay nada que revertir
99
+ 2. Consulta `_migrations ORDER BY migration DESC` para obtener las últimas N
100
+ 3. Por cada una, en transacción: `DELETE FROM _migrations` → ejecuta `down.sql` → `COMMIT`
101
+ 4. Si `down.sql` falla, hace `ROLLBACK` y para
102
+
103
+ **El `down.sql` debe existir.** Si no existe, el script para sin revertir nada.
104
+
105
+ ## Error común
106
+
107
+ Si `npm run migrate` dice que ya está al día pero los cambios no aparecen:
108
+ ```sql
109
+ DELETE FROM _migrations WHERE migration = 'NNN_YYYYMMDD_HHMM_nombre';
110
+ ```
111
+ Luego volver a correr `npm run migrate`. O crear una nueva migración con el cambio faltante.
@@ -0,0 +1,238 @@
1
+ ---
2
+ paths:
3
+ - "schema/**"
4
+ ---
5
+
6
+ # Reglas para trabajar en schema/
7
+
8
+ ## Regla fundamental de rutas en init.sql
9
+
10
+ **Todas las rutas en todos los `init.sql` deben ser absolutas desde la raíz del proyecto.**
11
+
12
+ psql resuelve los `\i` siempre desde su directorio de trabajo (CWD), no desde el directorio del archivo que se está leyendo. `build.py` también resuelve desde la raíz del proyecto (`ROOT_DIR`). Los dos se comportan igual — por eso todas las rutas deben ser desde la raíz.
13
+
14
+ ```sql
15
+ -- CORRECTO — ruta desde la raíz
16
+ \i schema/auth/roles/create.sql
17
+ \i schema/auth/roles/data.sql
18
+
19
+ -- INCORRECTO — ruta relativa al archivo
20
+ \i create.sql
21
+ \i ../roles/create.sql
22
+ ```
23
+
24
+ El usuario siempre debe estar parado en la raíz del proyecto al usar psql:
25
+ ```
26
+ \cd /ruta/al/proyecto
27
+ \i init.sql
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Cómo agregar una tabla standalone
33
+
34
+ ```bash
35
+ mkdir schema/mi-tabla
36
+ touch schema/mi-tabla/create.sql schema/mi-tabla/data.sql schema/mi-tabla/init.sql
37
+ ```
38
+
39
+ - `create.sql` → `CREATE TABLE` con columnas, constraints y FKs
40
+ - `data.sql` → INSERTs de datos iniciales (roles, estados, catálogos). Si no hay, dejarlo comentado — el archivo debe existir igual
41
+ - `init.sql` → rutas completas desde la raíz del proyecto:
42
+
43
+ ```sql
44
+ -- sin triggers
45
+ \i schema/mi-tabla/create.sql
46
+ \i schema/mi-tabla/data.sql
47
+
48
+ -- con triggers
49
+ \i schema/mi-tabla/create.sql
50
+ \i schema/mi-tabla/data.sql
51
+ \i schema/mi-tabla/triggers.sql
52
+ ```
53
+
54
+ - Agregar `\i schema/mi-tabla/init.sql` a `schema/init.sql` en la posición correcta
55
+
56
+ ## Cómo agregar un grupo
57
+
58
+ ```bash
59
+ mkdir schema/mi-grupo
60
+ touch schema/mi-grupo/init.sql
61
+ ```
62
+
63
+ El `init.sql` del grupo lista sus tablas con rutas completas desde la raíz: `\i schema/mi-grupo/mi-tabla/init.sql`. Agregar `\i schema/mi-grupo/init.sql` a `schema/init.sql`.
64
+
65
+ ## Cuándo crear un grupo vs standalone
66
+
67
+ | Standalone | Grupo |
68
+ |---|---|
69
+ | Tabla transversal a varios dominios (`parameters`, `tags`, `files`) | 2+ tablas del mismo dominio funcional |
70
+ | Una sola tabla en ese dominio | Tablas que solo tienen sentido juntas |
71
+
72
+ ## Lógica de agrupación por dominio
73
+
74
+ Agrupar por **responsabilidad funcional**, no por conveniencia técnica.
75
+
76
+ Grupos comunes:
77
+
78
+ | Grupo | Tablas típicas |
79
+ |---|---|
80
+ | `auth` | `users`, `roles`, `permissions`, `sessions` |
81
+ | `catalog` | `products`, `categories`, `brands` |
82
+ | `orders` | `orders`, `order_items`, `order_statuses` |
83
+ | `payments` | `payments`, `payment_methods`, `invoices` |
84
+ | `notifications` | `notifications`, `notification_types` |
85
+
86
+ Una tabla que tiene FK hacia tablas de **dos grupos distintos** probablemente va standalone, después de ambos grupos en `schema/init.sql`.
87
+
88
+ ## Formato estándar de CREATE TABLE
89
+
90
+ Este es el formato propio del proyecto. Toda tabla nueva debe seguirlo sin excepción.
91
+
92
+ ### Orden de columnas (de arriba hacia abajo)
93
+
94
+ ```
95
+ 1. PK → primera columna, siempre
96
+ 2. FKs → en columnas simples, sin referenciar aún, después del PK
97
+ 3. Atributos → campos propios de la tabla en orden lógico de importancia
98
+ 4. Timestamps → created_at y updated_at al final, siempre TIMESTAMPTZ
99
+ 5. Constraints → PRIMARY KEY primero, luego cada FOREIGN KEY con REFERENCES
100
+ ```
101
+
102
+ ### Naming
103
+
104
+ - **Tablas**: `snake_case`, **plural** — representan una colección de registros (`users`, `order_items`)
105
+ - **PK**: nombre de la tabla en singular + `_id` → `user_id`, `order_id`, `role_id`
106
+ - Si el nombre es largo, usar una versión simplificada que mantenga el contexto
107
+ - **FK columns**: mismo formato que el PK de la tabla que referencian → `role_id`, `created_by`
108
+ - Índices: `idx_{tabla}_{columna}` (`idx_users_email`)
109
+ - Triggers: `trg_{tabla}_{descripcion}`
110
+ - Funciones de trigger: `fn_{tabla}_{descripcion}`
111
+ - Constraints únicos: `uq_{tabla}_{columna}` (`uq_users_email`)
112
+
113
+ ### Ejemplo de referencia
114
+
115
+ ```sql
116
+ CREATE TABLE users (
117
+ user_id SERIAL,
118
+ role_id INT NOT NULL, -- rol asignado al usuario
119
+ created_by INT, -- usuario que lo creó (NULL solo para root)
120
+ full_name VARCHAR(150) NOT NULL, -- nombre completo del usuario
121
+ username VARCHAR(50) NOT NULL UNIQUE, -- nombre de usuario único
122
+ email VARCHAR(150), -- correo electrónico
123
+ password_hash VARCHAR(255) NOT NULL, -- contraseña almacenada con hash seguro
124
+ failed_attempts SMALLINT NOT NULL DEFAULT 0, -- contador de intentos de login fallidos
125
+ active BOOLEAN NOT NULL DEFAULT TRUE, -- cuenta activa/inactiva (no se elimina)
126
+ requires_pwd_change BOOLEAN NOT NULL DEFAULT TRUE, -- fuerza cambio de contraseña en primer ingreso
127
+ locked_until TIMESTAMPTZ, -- fecha y hora hasta cuando la cuenta está bloqueada
128
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
129
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
130
+ PRIMARY KEY (user_id),
131
+ FOREIGN KEY (role_id) REFERENCES roles(role_id),
132
+ FOREIGN KEY (created_by) REFERENCES users(user_id)
133
+ );
134
+ ```
135
+
136
+ ### Reglas del formato
137
+
138
+ - **PK se declara como columna simple** (`SERIAL`) sin `PRIMARY KEY` inline — la constraint va al final
139
+ - **FKs se declaran como columnas simples** (`INT`, `INT NOT NULL`) sin `REFERENCES` inline — la constraint va al final
140
+ - **Alineación visual**: columna, tipo, constraints y comentario alineados con espacios para legibilidad
141
+ - **Comentarios inline** (`--`) en toda columna que no sea autoexplicativa por su nombre
142
+ - **`NOT NULL` explícito** en toda columna que no admite nulos — nunca asumir por defecto
143
+ - **Timestamps**: siempre `TIMESTAMPTZ` (con zona horaria), nunca `TIMESTAMP` a secas
144
+ - **Orden de constraints al final**: primero `PRIMARY KEY`, luego `FOREIGN KEY` en el mismo orden que aparecen las columnas FK arriba
145
+
146
+ ### Tipos de datos
147
+
148
+ | Dato | Tipo |
149
+ |---|---|
150
+ | PK autoincremental | `SERIAL` |
151
+ | FK, referencia a otra tabla | `INT` |
152
+ | Texto con longitud conocida | `VARCHAR(N)` |
153
+ | Texto sin límite | `TEXT` |
154
+ | Contador pequeño | `SMALLINT` |
155
+ | Número entero general | `INT` |
156
+ | Dinero / precisión decimal | `NUMERIC(10,2)` — nunca `FLOAT` |
157
+ | Booleano | `BOOLEAN` — nunca `INT` ni `CHAR` |
158
+ | Solo fecha | `DATE` |
159
+ | Fecha + hora + zona | `TIMESTAMPTZ` — siempre esta, nunca `TIMESTAMP` a secas |
160
+ | Enumeraciones | tabla de catálogo con FK — nunca `ENUM` de Postgres (difícil de migrar) |
161
+
162
+ ### Columnas de auditoría
163
+
164
+ Toda tabla de datos (no catálogo puro) cierra con:
165
+ ```sql
166
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
167
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
168
+ ```
169
+
170
+ El `updated_at` se mantiene con un trigger en `triggers.sql`.
171
+ Si aplica borrado lógico, agregar `deleted_at TIMESTAMPTZ` antes de `created_at`.
172
+
173
+ ### Integridad referencial
174
+
175
+ Elegir `ON DELETE` conscientemente en cada FK:
176
+
177
+ - `CASCADE` → el hijo no tiene sentido sin el padre (`order_items` → `orders`)
178
+ - `SET NULL` → la relación es opcional (la columna FK debe admitir NULL)
179
+ - Sin cláusula (default `RESTRICT`) → el borrado del padre se bloquea explícitamente
180
+
181
+ ### Índices
182
+
183
+ - La PK ya crea índice automáticamente
184
+ - Crear índices en columnas frecuentes en `WHERE`, `JOIN ON`, `ORDER BY`
185
+ - Índice compuesto cuando las queries filtran por dos columnas juntas
186
+ - No sobre-indexar: cada índice tiene costo en escritura
187
+
188
+ ### Triggers
189
+
190
+ Solo crear `triggers.sql` si la tabla realmente lo necesita.
191
+ Siempre usar `CREATE OR REPLACE FUNCTION` para que sea re-ejecutable.
192
+
193
+ ---
194
+
195
+ ## Diccionario de tabla (DICTIONARY.md)
196
+
197
+ Cada carpeta de tabla debe tener un `DICTIONARY.md` junto a `create.sql`, `data.sql` e `init.sql`.
198
+
199
+ **Cuándo generarlo**: al crear una tabla nueva o al recibir un `create.sql` existente.
200
+ Si trabajo en una tabla que no tiene su `DICTIONARY.md`, preguntar si debo generarlo.
201
+
202
+ ### Plantilla
203
+
204
+ ```markdown
205
+ # {nombre_tabla}
206
+
207
+ {Descripción en 1-2 líneas: qué representa esta tabla en el dominio del negocio.}
208
+
209
+ ## Columnas
210
+
211
+ | Columna | Tipo | Nulo | Default | Descripción |
212
+ |---------|------|------|---------|-------------|
213
+ | {col} | {tipo} | NO/SÍ | {default o —} | {descripción en lenguaje de negocio} |
214
+
215
+ ## Relaciones
216
+
217
+ | Columna | Referencia | Descripción |
218
+ |---------|------------|-------------|
219
+ | {fk_col} | {tabla.col} | {qué representa esta relación en el negocio} |
220
+
221
+ ## Reglas de negocio
222
+
223
+ - {regla o restricción relevante para el negocio}
224
+ - {caso especial o comportamiento esperado}
225
+
226
+ ## Notas técnicas
227
+
228
+ - {constraints, triggers, índices relevantes que no son obvios del create.sql}
229
+ ```
230
+
231
+ ### Reglas del diccionario
232
+
233
+ - La descripción del header explica **qué representa** la tabla, no cómo está implementada
234
+ - Columnas: descripciones en **lenguaje de negocio**, no técnico ("Rol del usuario" no "FK a roles")
235
+ - Relaciones: solo las FK; explicar el **significado de la relación**, no solo el dato técnico
236
+ - Reglas de negocio: comportamiento esperado, casos borde, qué operaciones están prohibidas
237
+ - Notas técnicas: solo lo que no es visible a simple vista en el `create.sql` (triggers, índices, constraints nombrados)
238
+ - Si una sección no aplica (ej. tabla sin FK → sin sección Relaciones), omitirla completamente
@@ -0,0 +1,5 @@
1
+ DB_HOST=localhost
2
+ DB_PORT=5432
3
+ DB_NAME=your_database
4
+ DB_USER=your_user
5
+ DB_PASSWORD=your_password
@@ -0,0 +1,117 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Base de datos PostgreSQL — generada con [`@darkj/create-db`](https://www.npmjs.com/package/@darkj/create-db).
4
+
5
+ ---
6
+
7
+ ## Configuración inicial
8
+
9
+ ```bash
10
+ npm install
11
+ cp .env.example .env # completar con las credenciales reales
12
+ ```
13
+
14
+ `.env` nunca se sube al repo (está en `.gitignore`).
15
+
16
+ ---
17
+
18
+ ## Comandos
19
+
20
+ | Comando | Hace | Necesita DB |
21
+ |---|---|---|
22
+ | `npm run build` | Fusiona todo `schema/` en `db-output.sql` | No |
23
+ | `npm run migrate` | Aplica migraciones pendientes a una DB existente | Sí |
24
+ | `npm run rollback` | Revierte la última migración | Sí |
25
+ | `npm run rollback 3` | Revierte las últimas 3 migraciones | Sí |
26
+
27
+ ---
28
+
29
+ ## Dos flujos de trabajo
30
+
31
+ **DB vacía** (desarrollo, CI, entorno nuevo):
32
+ ```bash
33
+ psql -U user -d dbname -f init.sql
34
+ ```
35
+
36
+ **DB existente con datos** (producción, staging):
37
+ ```bash
38
+ npm run migrate
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Estructura
44
+
45
+ ```
46
+ {{PROJECT_NAME}}/
47
+ ├── init.sql ← DROP ALL + recarga schema/ desde cero
48
+ ├── schema/ ← diseño actual: tablas organizadas por dominio
49
+ │ └── init.sql ← orden de carga (dependencias primero)
50
+ ├── migrations/ ← historial de cambios incrementales
51
+ │ └── NNN_YYYYMMDD_HHMM_proposito/
52
+ │ ├── up.sql ← cambios a aplicar
53
+ │ └── down.sql ← cómo revertirlos
54
+ ├── scripts/ ← build.ts · migrate.ts · rollback.ts
55
+ └── backups/ ← dumps de pg_dump (gitignored)
56
+ ```
57
+
58
+ ### Organización de schema/
59
+
60
+ ```
61
+ schema/
62
+ ├── init.sql ← orden global
63
+ ├── tabla-standalone/ ← tabla transversal a varios dominios
64
+ │ ├── create.sql
65
+ │ ├── data.sql
66
+ │ └── init.sql
67
+ └── nombre-grupo/ ← dominio funcional (auth, orders, catalog…)
68
+ ├── init.sql
69
+ └── tabla/
70
+ ├── create.sql
71
+ ├── data.sql
72
+ ├── triggers.sql ← solo si aplica
73
+ └── init.sql
74
+ ```
75
+
76
+ > Todas las rutas dentro de los `init.sql` son absolutas desde la raíz del proyecto.
77
+ > `psql` resuelve `\i` desde su directorio de trabajo, no desde el archivo.
78
+
79
+ ---
80
+
81
+ ## Agregar una tabla
82
+
83
+ ```bash
84
+ mkdir schema/mi-tabla
85
+ touch schema/mi-tabla/create.sql schema/mi-tabla/data.sql schema/mi-tabla/init.sql
86
+ ```
87
+
88
+ `schema/mi-tabla/init.sql`:
89
+ ```sql
90
+ \i schema/mi-tabla/create.sql
91
+ \i schema/mi-tabla/data.sql
92
+ ```
93
+
94
+ Agregar a `schema/init.sql`:
95
+ ```sql
96
+ \i schema/mi-tabla/init.sql
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Crear una migración
102
+
103
+ ```bash
104
+ mkdir migrations/002_20260601_1000_descripcion
105
+ # escribir up.sql y down.sql
106
+ npm run migrate
107
+ ```
108
+
109
+ > Nunca editar un `up.sql` ya aplicado. Para corregir errores, crear una nueva migración.
110
+
111
+ ---
112
+
113
+ ## Backup
114
+
115
+ ```bash
116
+ pg_dump -U user -d dbname -F p -f backups/$(date +%Y%m%d_%H%M%S)_descripcion.sql
117
+ ```
File without changes
@@ -0,0 +1,14 @@
1
+ -- ==========================================================
2
+ -- RESET COMPLETO — elimina todo y recarga desde schema/
3
+ -- Usar solo en entornos vacíos o de desarrollo.
4
+ -- ==========================================================
5
+
6
+ DO $$ DECLARE
7
+ r RECORD;
8
+ BEGIN
9
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
10
+ EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
11
+ END LOOP;
12
+ END $$;
13
+
14
+ \i schema/init.sql
File without changes
@@ -0,0 +1,10 @@
1
+ -- ==========================================================
2
+ -- SCHEMA LOAD ORDER
3
+ -- Rutas siempre desde la raíz del proyecto. Dependencias primero.
4
+ -- ==========================================================
5
+
6
+ -- Tabla standalone (sin grupo):
7
+ -- \i schema/mi-tabla/init.sql
8
+
9
+ -- Grupo de tablas:
10
+ -- \i schema/mi-grupo/init.sql
@@ -0,0 +1,62 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ const ROOT_DIR = process.cwd();
5
+ const OUTPUT_FILE = "db-output.sql";
6
+ const ROOT_INIT = "init.sql";
7
+ const visited = new Set<string>();
8
+
9
+ function readLines(filePath: string): string[] {
10
+ if (!fs.existsSync(filePath)) {
11
+ console.warn(`[WARN] Not found: ${filePath}`);
12
+ return [];
13
+ }
14
+ return fs.readFileSync(filePath, "utf-8").split("\n").map((l) => l.trimEnd());
15
+ }
16
+
17
+ function sectionLabel(initPath: string): string {
18
+ const rel = path.relative(ROOT_DIR, path.dirname(path.resolve(initPath)));
19
+ return rel.replace(/[\\/]/g, " > ") || ".";
20
+ }
21
+
22
+ function processInit(initPath: string): string[] {
23
+ const absPath = path.resolve(initPath);
24
+ if (visited.has(absPath)) return [];
25
+ visited.add(absPath);
26
+
27
+ const output: string[] = [];
28
+ for (const line of readLines(absPath)) {
29
+ const match = line.match(/^\s*\\i\s+(.+)/);
30
+ if (!match) {
31
+ output.push(line);
32
+ continue;
33
+ }
34
+ const ref = match[1].trim();
35
+ const refPath = path.join(ROOT_DIR, ref);
36
+
37
+ if (ref.endsWith("init.sql")) {
38
+ const lbl = sectionLabel(refPath);
39
+ const dashes = "-".repeat(Math.max(2, 54 - lbl.length));
40
+ output.push(`\n-- [${lbl}] ${dashes}`);
41
+ output.push(...processInit(refPath));
42
+ output.push(`-- [/${lbl}]\n`);
43
+ } else {
44
+ output.push(...readLines(refPath));
45
+ }
46
+ }
47
+ return output;
48
+ }
49
+
50
+ const now = new Date().toISOString().replace("T", " ").slice(0, 19);
51
+ const header = [
52
+ "-- " + "=".repeat(58),
53
+ `-- Generated: ${now}`,
54
+ "-- DO NOT EDIT — run: npm run build",
55
+ "-- " + "=".repeat(58),
56
+ "",
57
+ ];
58
+
59
+ if (fs.existsSync(OUTPUT_FILE)) fs.unlinkSync(OUTPUT_FILE);
60
+ const lines = processInit(ROOT_INIT);
61
+ fs.writeFileSync(OUTPUT_FILE, [...header, ...lines].join("\n") + "\n", "utf-8");
62
+ console.log(`Generated: ${OUTPUT_FILE}`);
@@ -0,0 +1,101 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { Client } from "pg";
4
+
5
+ const ROOT_DIR = process.cwd();
6
+ const MIGRATIONS_DIR = path.join(ROOT_DIR, "migrations");
7
+
8
+ function loadEnv(): Record<string, string> {
9
+ const envPath = path.join(ROOT_DIR, ".env");
10
+ if (!fs.existsSync(envPath)) {
11
+ console.error(".env not found. Copy .env.example and fill in your credentials.");
12
+ process.exit(1);
13
+ }
14
+ const env: Record<string, string> = {};
15
+ for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith("#")) continue;
18
+ const eq = trimmed.indexOf("=");
19
+ if (eq === -1) continue;
20
+ env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
21
+ }
22
+ return env;
23
+ }
24
+
25
+ async function ensureMigrationsTable(client: Client): Promise<void> {
26
+ await client.query(`
27
+ CREATE TABLE IF NOT EXISTS _migrations (
28
+ migration VARCHAR(255) PRIMARY KEY,
29
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
30
+ )
31
+ `);
32
+ }
33
+
34
+ async function appliedMigrations(client: Client): Promise<Set<string>> {
35
+ const result = await client.query<{ migration: string }>(
36
+ "SELECT migration FROM _migrations ORDER BY migration"
37
+ );
38
+ return new Set(result.rows.map((r) => r.migration));
39
+ }
40
+
41
+ function pendingMigrations(applied: Set<string>): string[] {
42
+ if (!fs.existsSync(MIGRATIONS_DIR)) return [];
43
+ return fs
44
+ .readdirSync(MIGRATIONS_DIR)
45
+ .filter((name) => {
46
+ const dir = path.join(MIGRATIONS_DIR, name);
47
+ return (
48
+ fs.statSync(dir).isDirectory() &&
49
+ !applied.has(name) &&
50
+ fs.existsSync(path.join(dir, "up.sql"))
51
+ );
52
+ })
53
+ .sort();
54
+ }
55
+
56
+ async function main(): Promise<void> {
57
+ const env = loadEnv();
58
+ const client = new Client({
59
+ host: env.DB_HOST ?? "localhost",
60
+ port: Number(env.DB_PORT ?? 5432),
61
+ database: env.DB_NAME,
62
+ user: env.DB_USER,
63
+ password: env.DB_PASSWORD,
64
+ });
65
+
66
+ await client.connect();
67
+ await ensureMigrationsTable(client);
68
+
69
+ const applied = await appliedMigrations(client);
70
+ const pending = pendingMigrations(applied);
71
+
72
+ if (pending.length === 0) {
73
+ console.log("Nothing to migrate.");
74
+ await client.end();
75
+ return;
76
+ }
77
+
78
+ for (const name of pending) {
79
+ const upPath = path.join(MIGRATIONS_DIR, name, "up.sql");
80
+ const sql = fs.readFileSync(upPath, "utf-8");
81
+ console.log(`Applying: ${name}`);
82
+ await client.query("BEGIN");
83
+ try {
84
+ await client.query(sql);
85
+ await client.query("INSERT INTO _migrations (migration) VALUES ($1)", [name]);
86
+ await client.query("COMMIT");
87
+ console.log(` ✓ Done`);
88
+ } catch (err) {
89
+ await client.query("ROLLBACK");
90
+ console.error(` ✗ Failed — rolled back`);
91
+ console.error(err);
92
+ await client.end();
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ console.log(`\nApplied ${pending.length} migration(s).`);
98
+ await client.end();
99
+ }
100
+
101
+ main();
@@ -0,0 +1,91 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { Client } from "pg";
4
+
5
+ const ROOT_DIR = process.cwd();
6
+ const MIGRATIONS_DIR = path.join(ROOT_DIR, "migrations");
7
+
8
+ function loadEnv(): Record<string, string> {
9
+ const envPath = path.join(ROOT_DIR, ".env");
10
+ if (!fs.existsSync(envPath)) {
11
+ console.error(".env not found.");
12
+ process.exit(1);
13
+ }
14
+ const env: Record<string, string> = {};
15
+ for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith("#")) continue;
18
+ const eq = trimmed.indexOf("=");
19
+ if (eq === -1) continue;
20
+ env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
21
+ }
22
+ return env;
23
+ }
24
+
25
+ async function main(): Promise<void> {
26
+ const steps = Number(process.argv[2] ?? 1);
27
+ if (!Number.isInteger(steps) || steps < 1) {
28
+ console.error("Usage: npm run rollback [N] (N = number of migrations to revert, default 1)");
29
+ process.exit(1);
30
+ }
31
+
32
+ const env = loadEnv();
33
+ const client = new Client({
34
+ host: env.DB_HOST ?? "localhost",
35
+ port: Number(env.DB_PORT ?? 5432),
36
+ database: env.DB_NAME,
37
+ user: env.DB_USER,
38
+ password: env.DB_PASSWORD,
39
+ });
40
+
41
+ await client.connect();
42
+
43
+ const tableCheck = await client.query<{ exists: boolean }>(
44
+ "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = '_migrations') AS exists"
45
+ );
46
+ if (!tableCheck.rows[0].exists) {
47
+ console.log("No applied migrations to revert.");
48
+ await client.end();
49
+ return;
50
+ }
51
+
52
+ const result = await client.query<{ migration: string }>(
53
+ "SELECT migration FROM _migrations ORDER BY migration DESC LIMIT $1",
54
+ [steps]
55
+ );
56
+
57
+ if (result.rows.length === 0) {
58
+ console.log("No applied migrations to revert.");
59
+ await client.end();
60
+ return;
61
+ }
62
+
63
+ for (const { migration } of result.rows) {
64
+ const downPath = path.join(MIGRATIONS_DIR, migration, "down.sql");
65
+ if (!fs.existsSync(downPath)) {
66
+ console.error(`No down.sql for migration: ${migration}`);
67
+ await client.end();
68
+ process.exit(1);
69
+ }
70
+ const sql = fs.readFileSync(downPath, "utf-8");
71
+ console.log(`Reverting: ${migration}`);
72
+ await client.query("BEGIN");
73
+ try {
74
+ await client.query("DELETE FROM _migrations WHERE migration = $1", [migration]);
75
+ await client.query(sql);
76
+ await client.query("COMMIT");
77
+ console.log(` ✓ Reverted`);
78
+ } catch (err) {
79
+ await client.query("ROLLBACK");
80
+ console.error(` ✗ Failed — rolled back`);
81
+ console.error(err);
82
+ await client.end();
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ console.log(`\nReverted ${result.rows.length} migration(s).`);
88
+ await client.end();
89
+ }
90
+
91
+ main();
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "outDir": "dist"
9
+ },
10
+ "include": ["scripts/**/*.ts"]
11
+ }