@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 +70 -0
- package/dist/generator.js +86 -0
- package/dist/index.js +78 -0
- package/package.json +32 -0
- package/templates/base/.claude/CLAUDE.md +83 -0
- package/templates/base/.claude/commands/analyze.md +18 -0
- package/templates/base/.claude/commands/backup.md +19 -0
- package/templates/base/.claude/commands/build.md +17 -0
- package/templates/base/.claude/commands/new-dictionary.md +44 -0
- package/templates/base/.claude/commands/new-migration.md +61 -0
- package/templates/base/.claude/rules/migrations.md +111 -0
- package/templates/base/.claude/rules/schema.md +238 -0
- package/templates/base/.env.example +5 -0
- package/templates/base/README.md +117 -0
- package/templates/base/backups/.gitkeep +0 -0
- package/templates/base/init.sql +14 -0
- package/templates/base/migrations/.gitkeep +0 -0
- package/templates/base/schema/init.sql +10 -0
- package/templates/base/scripts/build.ts +62 -0
- package/templates/base/scripts/migrate.ts +101 -0
- package/templates/base/scripts/rollback.ts +91 -0
- package/templates/base/tsconfig.json +11 -0
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,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();
|