migrasafe 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/LICENSE +21 -0
- package/README.md +179 -0
- package/dist/checker/checker.d.ts +5 -0
- package/dist/checker/checker.d.ts.map +1 -0
- package/dist/checker/checker.js +185 -0
- package/dist/checker/checker.js.map +1 -0
- package/dist/checker/rules.d.ts +2 -0
- package/dist/checker/rules.d.ts.map +1 -0
- package/dist/checker/rules.js +261 -0
- package/dist/checker/rules.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/output/formatter.d.ts +3 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +91 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 febrifelis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# migrasafe
|
|
2
|
+
|
|
3
|
+
> Detect unsafe SQL migrations before deploying to production.
|
|
4
|
+
|
|
5
|
+
Zero external dependencies. Single binary. Works in any CI pipeline.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/migrasafe)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## The Problem
|
|
13
|
+
|
|
14
|
+
Running a bad migration in production can cause data loss, table locks, or downtime — often irreversible. `migrasafe` catches these issues **before** they reach your database.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Run without installing
|
|
22
|
+
npx migrasafe check ./migrations/
|
|
23
|
+
|
|
24
|
+
# Or install globally
|
|
25
|
+
npm install -g migrasafe
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Check a single file
|
|
34
|
+
migrasafe check migration.sql
|
|
35
|
+
|
|
36
|
+
# Check a directory (scans all .sql files, sorted by name)
|
|
37
|
+
migrasafe check ./migrations/
|
|
38
|
+
|
|
39
|
+
# JSON output — for CI pipelines and scripting
|
|
40
|
+
migrasafe check ./migrations/ --format json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Exit codes:** `0` = safe, `1` = unsafe or error.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Example Output
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Scanning 3 file(s)...
|
|
51
|
+
|
|
52
|
+
migrations/V2__add_status.sql
|
|
53
|
+
⚠ HIGH Line 1
|
|
54
|
+
Statement: ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL
|
|
55
|
+
Problem : ADD COLUMN NOT NULL without DEFAULT will fail on non-empty tables.
|
|
56
|
+
Fix : Use 3 steps: (1) ADD COLUMN nullable, (2) backfill data, (3) SET NOT NULL.
|
|
57
|
+
|
|
58
|
+
✖ CRITICAL Line 2
|
|
59
|
+
Statement: DROP TABLE old_orders
|
|
60
|
+
Problem : DROP TABLE is irreversible — all data will be permanently lost.
|
|
61
|
+
Fix : Use soft-delete or rename the table first, then drop it in a later migration.
|
|
62
|
+
|
|
63
|
+
── Summary ──────────────────────────────
|
|
64
|
+
CRITICAL : 1
|
|
65
|
+
HIGH : 1
|
|
66
|
+
Total : 2 issue(s) across 3 file(s)
|
|
67
|
+
|
|
68
|
+
✖ UNSAFE — resolve all CRITICAL/HIGH issues before deploying
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Rules
|
|
74
|
+
|
|
75
|
+
| Rule | Severity | Why |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `DROP DATABASE` | CRITICAL | Destroys the entire database and all its data |
|
|
78
|
+
| `ALTER SYSTEM` | CRITICAL | Modifies server config — wrong value can crash the server |
|
|
79
|
+
| `DROP OWNED BY` | CRITICAL | Silently drops all objects owned by a role |
|
|
80
|
+
| `DROP TABLE` | CRITICAL | Irreversible — all data permanently lost |
|
|
81
|
+
| `DROP SCHEMA` | CRITICAL | Irreversible — all tables, views, and data lost |
|
|
82
|
+
| `DROP COLUMN` | CRITICAL | Irreversible — column data permanently lost |
|
|
83
|
+
| `TRUNCATE` | CRITICAL | Deletes all rows immediately |
|
|
84
|
+
| `DELETE` without `WHERE` | CRITICAL | Deletes all rows |
|
|
85
|
+
| `UPDATE` without `WHERE` | HIGH | Modifies all rows |
|
|
86
|
+
| `RENAME TABLE` | HIGH | Breaking change for existing queries |
|
|
87
|
+
| `RENAME COLUMN` | HIGH | Breaking change for existing queries |
|
|
88
|
+
| `ADD COLUMN NOT NULL` without `DEFAULT` | HIGH | Fails on non-empty tables |
|
|
89
|
+
| `ALTER COLUMN TYPE` | HIGH | May fail if data cannot be cast |
|
|
90
|
+
| `ALTER COLUMN SET NOT NULL` | HIGH | Fails if any row has NULL in that column |
|
|
91
|
+
| `CREATE INDEX` without `CONCURRENTLY` | MEDIUM | Locks table during index build |
|
|
92
|
+
| `DROP INDEX` | MEDIUM | May degrade query performance |
|
|
93
|
+
| `DROP CONSTRAINT` | MEDIUM | Removes data validation |
|
|
94
|
+
| `ADD UNIQUE CONSTRAINT` | MEDIUM | Fails if duplicate values exist |
|
|
95
|
+
| `ADD CHECK CONSTRAINT` | MEDIUM | Fails if existing rows violate the constraint |
|
|
96
|
+
| `DROP SEQUENCE` | MEDIUM | May break auto-increment or application code |
|
|
97
|
+
| `DROP TYPE` | MEDIUM | May break columns or functions using this type |
|
|
98
|
+
| `LOCK TABLE` | MEDIUM | Blocks all reads and writes during the lock |
|
|
99
|
+
| `CLUSTER` | MEDIUM | Locks the table for the full duration of the operation |
|
|
100
|
+
| `REINDEX` without `CONCURRENTLY` | MEDIUM | Locks index and blocks reads/writes |
|
|
101
|
+
| `DETACH PARTITION` | MEDIUM | May break queries targeting the partition |
|
|
102
|
+
| `ALTER TABLE DISABLE TRIGGER` | HIGH | Bypasses trigger-based validation — may allow dirty data |
|
|
103
|
+
| `VACUUM FULL` | MEDIUM | Locks the table exclusively for the full duration |
|
|
104
|
+
| `DROP DOMAIN` | MEDIUM | May break columns or functions that use this domain |
|
|
105
|
+
| `DROP AGGREGATE` | MEDIUM | May break queries that use this aggregate function |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## CI Integration
|
|
110
|
+
|
|
111
|
+
### GitHub Actions
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
- name: Check migration safety
|
|
115
|
+
run: npx migrasafe check ./migrations/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### GitLab CI
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
check-migrations:
|
|
122
|
+
script:
|
|
123
|
+
- npx migrasafe check ./migrations/
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Pre-commit hook
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
#!/bin/sh
|
|
130
|
+
npx migrasafe check ./migrations/
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## JSON Output
|
|
136
|
+
|
|
137
|
+
Use `--format json` for structured output in scripts or dashboards:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"safe": false,
|
|
142
|
+
"summary": {
|
|
143
|
+
"critical": 1,
|
|
144
|
+
"high": 1,
|
|
145
|
+
"medium": 0,
|
|
146
|
+
"total": 2
|
|
147
|
+
},
|
|
148
|
+
"files": [
|
|
149
|
+
{
|
|
150
|
+
"file": "migrations/V2__add_status.sql",
|
|
151
|
+
"issueCount": 2,
|
|
152
|
+
"issues": [
|
|
153
|
+
{
|
|
154
|
+
"severity": "CRITICAL",
|
|
155
|
+
"line": 2,
|
|
156
|
+
"statement": "DROP TABLE old_orders",
|
|
157
|
+
"message": "DROP TABLE is irreversible — all data will be permanently lost.",
|
|
158
|
+
"suggestion": "Use soft-delete or rename the table first, then drop it in a later migration."
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## What It Does NOT Flag
|
|
169
|
+
|
|
170
|
+
- `DELETE FROM table WHERE condition` — safe, has a WHERE clause
|
|
171
|
+
- `UPDATE table SET col = val WHERE condition` — safe, has a WHERE clause
|
|
172
|
+
- `CREATE INDEX CONCURRENTLY` — safe, no table lock
|
|
173
|
+
- Keywords inside SQL string literals or comments — ignored
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
[MIT](./LICENSE) © [febrifelis](https://github.com/febrifelis)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CheckResult, ScanResult } from "../types";
|
|
2
|
+
import { MigrasafeConfig } from "../config";
|
|
3
|
+
export declare function checkFile(filePath: string, config?: MigrasafeConfig): CheckResult;
|
|
4
|
+
export declare function checkDirectory(dirPath: string, config?: MigrasafeConfig): CheckResult[];
|
|
5
|
+
export declare function buildScanResult(results: CheckResult[]): ScanResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../../src/checker/checker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAoEnD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAQvD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAO7D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,UAAU,CAclE"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkFile = checkFile;
|
|
7
|
+
exports.checkDirectory = checkDirectory;
|
|
8
|
+
exports.buildScanResult = buildScanResult;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const rules_1 = require("./rules");
|
|
12
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
13
|
+
const MAX_STATEMENTS = 10000;
|
|
14
|
+
function splitStatements(sql) {
|
|
15
|
+
const results = [];
|
|
16
|
+
let current = "";
|
|
17
|
+
let startLine = 1;
|
|
18
|
+
let currentLine = 1;
|
|
19
|
+
let inLineComment = false;
|
|
20
|
+
let inBlockComment = false;
|
|
21
|
+
let inSingleQuote = false;
|
|
22
|
+
let inDoubleQuote = false;
|
|
23
|
+
let dollarTag = "";
|
|
24
|
+
let inDollarQuote = false;
|
|
25
|
+
for (let i = 0; i < sql.length; i++) {
|
|
26
|
+
const ch = sql[i];
|
|
27
|
+
const next = sql[i + 1];
|
|
28
|
+
if (ch === "\n") {
|
|
29
|
+
inLineComment = false;
|
|
30
|
+
currentLine++;
|
|
31
|
+
}
|
|
32
|
+
// --- dollar-quoted strings (PostgreSQL) ---
|
|
33
|
+
if (!inLineComment && !inBlockComment && !inSingleQuote && !inDoubleQuote) {
|
|
34
|
+
if (!inDollarQuote && ch === "$") {
|
|
35
|
+
const rest = sql.slice(i);
|
|
36
|
+
const match = rest.match(/^\$([^$]*)\$/);
|
|
37
|
+
if (match) {
|
|
38
|
+
dollarTag = match[0];
|
|
39
|
+
current += dollarTag;
|
|
40
|
+
i += dollarTag.length - 1;
|
|
41
|
+
inDollarQuote = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (inDollarQuote) {
|
|
46
|
+
const rest = sql.slice(i);
|
|
47
|
+
if (rest.startsWith(dollarTag)) {
|
|
48
|
+
current += dollarTag;
|
|
49
|
+
i += dollarTag.length - 1;
|
|
50
|
+
inDollarQuote = false;
|
|
51
|
+
dollarTag = "";
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
current += ch;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (inDollarQuote) {
|
|
60
|
+
current += ch;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// --- single-quoted strings ---
|
|
64
|
+
if (!inLineComment && !inBlockComment && !inDoubleQuote) {
|
|
65
|
+
if (ch === "'" && !inSingleQuote) {
|
|
66
|
+
inSingleQuote = true;
|
|
67
|
+
current += ch;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (inSingleQuote) {
|
|
71
|
+
current += ch;
|
|
72
|
+
// handle escaped quote ''
|
|
73
|
+
if (ch === "'" && next === "'") {
|
|
74
|
+
current += next;
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
else if (ch === "'") {
|
|
78
|
+
inSingleQuote = false;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// --- double-quoted identifiers ---
|
|
84
|
+
if (!inLineComment && !inBlockComment && !inSingleQuote) {
|
|
85
|
+
if (ch === '"' && !inDoubleQuote) {
|
|
86
|
+
inDoubleQuote = true;
|
|
87
|
+
current += ch;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (inDoubleQuote) {
|
|
91
|
+
current += ch;
|
|
92
|
+
if (ch === '"') {
|
|
93
|
+
inDoubleQuote = false;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (inLineComment)
|
|
99
|
+
continue;
|
|
100
|
+
if (inBlockComment) {
|
|
101
|
+
if (ch === "*" && next === "/") {
|
|
102
|
+
inBlockComment = false;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (ch === "-" && next === "-") {
|
|
108
|
+
inLineComment = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ch === "/" && next === "*") {
|
|
112
|
+
inBlockComment = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ch === ";") {
|
|
116
|
+
const trimmed = current.trim();
|
|
117
|
+
if (trimmed)
|
|
118
|
+
results.push({ statement: trimmed, line: startLine });
|
|
119
|
+
current = "";
|
|
120
|
+
startLine = currentLine + (next === "\n" ? 1 : 0);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
current += ch;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const remaining = current.trim();
|
|
127
|
+
if (remaining)
|
|
128
|
+
results.push({ statement: remaining, line: startLine });
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
function checkFile(filePath, config = {}) {
|
|
132
|
+
const stat = fs_1.default.statSync(filePath);
|
|
133
|
+
// Guard: skip symlinks
|
|
134
|
+
if (stat.isSymbolicLink()) {
|
|
135
|
+
throw new Error(`Skipping symlink: ${filePath}`);
|
|
136
|
+
}
|
|
137
|
+
// Guard: file size limit
|
|
138
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
139
|
+
throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB > 10 MB limit): ${filePath}`);
|
|
140
|
+
}
|
|
141
|
+
const raw = fs_1.default.readFileSync(filePath, "utf-8");
|
|
142
|
+
// Guard: binary file detection (null bytes)
|
|
143
|
+
if (raw.includes("\0")) {
|
|
144
|
+
throw new Error(`Binary file skipped: ${filePath}`);
|
|
145
|
+
}
|
|
146
|
+
const content = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^/, "");
|
|
147
|
+
const fileName = path_1.default.basename(filePath);
|
|
148
|
+
const statements = splitStatements(content);
|
|
149
|
+
// Guard: max statement count
|
|
150
|
+
if (statements.length > MAX_STATEMENTS) {
|
|
151
|
+
throw new Error(`Too many statements (${statements.length} > ${MAX_STATEMENTS} limit): ${filePath}`);
|
|
152
|
+
}
|
|
153
|
+
const issues = statements.flatMap(({ statement, line }) => (0, rules_1.checkStatement)(statement, line, fileName, config.disableRules));
|
|
154
|
+
return { file: filePath, issues };
|
|
155
|
+
}
|
|
156
|
+
function checkDirectory(dirPath, config = {}) {
|
|
157
|
+
const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
158
|
+
const ignorePatterns = (config.ignore ?? []).map((p) => new RegExp(p));
|
|
159
|
+
const sqlFiles = entries
|
|
160
|
+
.filter((e) => {
|
|
161
|
+
if (!e.isFile() || e.isSymbolicLink())
|
|
162
|
+
return false;
|
|
163
|
+
if (!e.name.toLowerCase().endsWith(".sql"))
|
|
164
|
+
return false;
|
|
165
|
+
const filePath = path_1.default.join(dirPath, e.name);
|
|
166
|
+
return !ignorePatterns.some((re) => re.test(filePath));
|
|
167
|
+
})
|
|
168
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
169
|
+
return sqlFiles.map((e) => checkFile(path_1.default.join(dirPath, e.name), config));
|
|
170
|
+
}
|
|
171
|
+
function buildScanResult(results) {
|
|
172
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
173
|
+
const criticalCount = allIssues.filter((i) => i.severity === "CRITICAL").length;
|
|
174
|
+
const highCount = allIssues.filter((i) => i.severity === "HIGH").length;
|
|
175
|
+
const mediumCount = allIssues.filter((i) => i.severity === "MEDIUM").length;
|
|
176
|
+
return {
|
|
177
|
+
results,
|
|
178
|
+
totalIssues: allIssues.length,
|
|
179
|
+
criticalCount,
|
|
180
|
+
highCount,
|
|
181
|
+
mediumCount,
|
|
182
|
+
safe: criticalCount === 0 && highCount === 0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checker.js","sourceRoot":"","sources":["../../src/checker/checker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/checker/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAY,MAAM,UAAU,CAAC;AAiG3C,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,KAAK,EAAE,CAmBT"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkStatement = checkStatement;
|
|
4
|
+
const RULES = [
|
|
5
|
+
{
|
|
6
|
+
id: "DROP_DATABASE",
|
|
7
|
+
severity: "CRITICAL",
|
|
8
|
+
pattern: /\bDROP\s+DATABASE\b/i,
|
|
9
|
+
message: "DROP DATABASE is irreversible — the entire database and all its data will be permanently destroyed.",
|
|
10
|
+
suggestion: "Never run DROP DATABASE in a migration file. Back up the database and confirm with your team before dropping.",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "ALTER_SYSTEM",
|
|
14
|
+
severity: "CRITICAL",
|
|
15
|
+
pattern: /\bALTER\s+SYSTEM\b/i,
|
|
16
|
+
message: "ALTER SYSTEM modifies postgresql.conf directly — a wrong value can make the server unbootable.",
|
|
17
|
+
suggestion: "Never place ALTER SYSTEM in a migration file. Apply server config changes through your infrastructure tooling.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "DROP_OWNED",
|
|
21
|
+
severity: "CRITICAL",
|
|
22
|
+
pattern: /\bDROP\s+OWNED\s+BY\b/i,
|
|
23
|
+
message: "DROP OWNED BY drops all objects owned by a role — this can silently destroy tables, sequences, and functions.",
|
|
24
|
+
suggestion: "Audit exactly which objects the role owns before running DROP OWNED BY.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "DROP_TABLE",
|
|
28
|
+
severity: "CRITICAL",
|
|
29
|
+
pattern: /\bDROP\s+TABLE\b/i,
|
|
30
|
+
message: "DROP TABLE is irreversible — all data will be permanently lost.",
|
|
31
|
+
suggestion: "Use soft-delete or rename the table first, then drop it in a later migration.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "DROP_SCHEMA",
|
|
35
|
+
severity: "CRITICAL",
|
|
36
|
+
pattern: /\bDROP\s+SCHEMA\b/i,
|
|
37
|
+
message: "DROP SCHEMA is irreversible — all tables, views, and data in the schema will be lost.",
|
|
38
|
+
suggestion: "Ensure all objects in the schema are migrated or backed up before dropping.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "DROP_COLUMN",
|
|
42
|
+
severity: "CRITICAL",
|
|
43
|
+
pattern: /\bDROP\s+COLUMN\b/i,
|
|
44
|
+
message: "DROP COLUMN is irreversible — all column data will be permanently lost.",
|
|
45
|
+
suggestion: "Ensure no application code references this column before dropping it.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "TRUNCATE",
|
|
49
|
+
severity: "CRITICAL",
|
|
50
|
+
pattern: /\bTRUNCATE\b/i,
|
|
51
|
+
message: "TRUNCATE will delete all rows in the table immediately.",
|
|
52
|
+
suggestion: "Do not run TRUNCATE in production unless absolutely intentional.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "DELETE_WITHOUT_WHERE",
|
|
56
|
+
severity: "CRITICAL",
|
|
57
|
+
pattern: /\bDELETE\s+FROM\s+\S+\s*(?:;|$)/i,
|
|
58
|
+
message: "DELETE without WHERE will remove every row in the table.",
|
|
59
|
+
suggestion: "Add a WHERE clause to target only the intended rows.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "UPDATE_WITHOUT_WHERE",
|
|
63
|
+
severity: "HIGH",
|
|
64
|
+
pattern: /\bUPDATE\s+\S+\s+SET\b(?![\s\S]*\bWHERE\b)/i,
|
|
65
|
+
message: "UPDATE without WHERE will modify every row in the table.",
|
|
66
|
+
suggestion: "Add a WHERE clause to target only the intended rows.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "RENAME_TABLE",
|
|
70
|
+
severity: "HIGH",
|
|
71
|
+
pattern: /\bRENAME\s+TABLE\b|\bALTER\s+TABLE\s+\S+\s+RENAME\s+TO\b/i,
|
|
72
|
+
message: "RENAME TABLE is a breaking change — queries using the old name will fail.",
|
|
73
|
+
suggestion: "Create a view with the old name, or update all references before renaming.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "RENAME_COLUMN",
|
|
77
|
+
severity: "HIGH",
|
|
78
|
+
pattern: /\bRENAME\s+COLUMN\b/i,
|
|
79
|
+
message: "RENAME COLUMN is a breaking change — queries using the old name will fail.",
|
|
80
|
+
suggestion: "Add the new column, backfill data, then drop the old column in a separate migration.",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "ADD_NOT_NULL_WITHOUT_DEFAULT",
|
|
84
|
+
severity: "HIGH",
|
|
85
|
+
pattern: /\bADD\s+COLUMN\s+\S+[\s\S]+?\bNOT\s+NULL\b(?![\s\S]*\bDEFAULT\b)/i,
|
|
86
|
+
message: "ADD COLUMN NOT NULL without DEFAULT will fail on non-empty tables.",
|
|
87
|
+
suggestion: "Use 3 steps: (1) ADD COLUMN nullable, (2) backfill data, (3) SET NOT NULL.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "ALTER_COLUMN_TYPE",
|
|
91
|
+
severity: "HIGH",
|
|
92
|
+
pattern: /\bALTER\s+COLUMN\s+\S+\s+(?:TYPE|SET\s+DATA\s+TYPE)\b/i,
|
|
93
|
+
message: "ALTER COLUMN TYPE may fail if existing data cannot be cast to the new type.",
|
|
94
|
+
suggestion: "Add a USING clause or manually migrate data before changing the column type.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "ALTER_COLUMN_SET_NOT_NULL",
|
|
98
|
+
severity: "HIGH",
|
|
99
|
+
pattern: /\bALTER\s+COLUMN\s+\S+\s+SET\s+NOT\s+NULL\b/i,
|
|
100
|
+
message: "ALTER COLUMN SET NOT NULL will fail if any existing rows contain NULL in that column.",
|
|
101
|
+
suggestion: "Backfill NULLs first: UPDATE table SET col = default WHERE col IS NULL, then SET NOT NULL.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "CREATE_INDEX_WITHOUT_CONCURRENTLY",
|
|
105
|
+
severity: "MEDIUM",
|
|
106
|
+
pattern: /\bCREATE\s+(?:UNIQUE\s+)?INDEX\b(?!\s+CONCURRENTLY)/i,
|
|
107
|
+
message: "CREATE INDEX without CONCURRENTLY locks the table and blocks reads/writes.",
|
|
108
|
+
suggestion: "Use CREATE INDEX CONCURRENTLY to avoid table lock in production.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "DROP_INDEX",
|
|
112
|
+
severity: "MEDIUM",
|
|
113
|
+
pattern: /\bDROP\s+INDEX\b/i,
|
|
114
|
+
message: "DROP INDEX may degrade query performance for queries that rely on this index.",
|
|
115
|
+
suggestion: "Verify no critical queries depend on this index before dropping it.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "DROP_CONSTRAINT",
|
|
119
|
+
severity: "MEDIUM",
|
|
120
|
+
pattern: /\bDROP\s+CONSTRAINT\b/i,
|
|
121
|
+
message: "DROP CONSTRAINT removes data validation — may allow dirty data into the table.",
|
|
122
|
+
suggestion: "Ensure application-level validation is in place before dropping the constraint.",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "ADD_UNIQUE_CONSTRAINT",
|
|
126
|
+
severity: "MEDIUM",
|
|
127
|
+
pattern: /\bADD\s+CONSTRAINT\s+\S+\s+UNIQUE\b/i,
|
|
128
|
+
message: "ADD UNIQUE CONSTRAINT will fail if duplicate values exist in the column(s).",
|
|
129
|
+
suggestion: "Check for and remove duplicate values before adding the unique constraint.",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "DROP_SEQUENCE",
|
|
133
|
+
severity: "MEDIUM",
|
|
134
|
+
pattern: /\bDROP\s+SEQUENCE\b/i,
|
|
135
|
+
message: "DROP SEQUENCE may break auto-increment columns or application code that relies on it.",
|
|
136
|
+
suggestion: "Ensure no tables or application code reference this sequence before dropping it.",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "DROP_TYPE",
|
|
140
|
+
severity: "MEDIUM",
|
|
141
|
+
pattern: /\bDROP\s+TYPE\b/i,
|
|
142
|
+
message: "DROP TYPE may break columns or functions that use this type.",
|
|
143
|
+
suggestion: "Ensure no tables, functions, or application code reference this type before dropping it.",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "ADD_CHECK_CONSTRAINT",
|
|
147
|
+
severity: "MEDIUM",
|
|
148
|
+
pattern: /\bADD\s+CONSTRAINT\s+\S+\s+CHECK\b/i,
|
|
149
|
+
message: "ADD CHECK CONSTRAINT will fail if existing rows violate the constraint.",
|
|
150
|
+
suggestion: "Verify all existing rows satisfy the check condition before adding the constraint.",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "LOCK_TABLE",
|
|
154
|
+
severity: "MEDIUM",
|
|
155
|
+
pattern: /\bLOCK\s+TABLE\b/i,
|
|
156
|
+
message: "LOCK TABLE blocks all reads and writes for the duration of the lock.",
|
|
157
|
+
suggestion: "Minimize lock duration and run during low-traffic windows.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "CLUSTER",
|
|
161
|
+
severity: "MEDIUM",
|
|
162
|
+
pattern: /\bCLUSTER\s+\S+\s+USING\b|\bCLUSTER\s+\S+\s*;/i,
|
|
163
|
+
message: "CLUSTER rewrites the table in index order and holds an exclusive lock throughout.",
|
|
164
|
+
suggestion: "Run CLUSTER during a maintenance window or consider pg_repack for online reordering.",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "REINDEX_WITHOUT_CONCURRENTLY",
|
|
168
|
+
severity: "MEDIUM",
|
|
169
|
+
pattern: /\bREINDEX\b(?!\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\s+CONCURRENTLY)/i,
|
|
170
|
+
message: "REINDEX without CONCURRENTLY locks the index and blocks reads/writes.",
|
|
171
|
+
suggestion: "Use REINDEX INDEX CONCURRENTLY to avoid locking.",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "DETACH_PARTITION",
|
|
175
|
+
severity: "MEDIUM",
|
|
176
|
+
pattern: /\bDETACH\s+PARTITION\b/i,
|
|
177
|
+
message: "DETACH PARTITION may break queries and application code targeting that partition.",
|
|
178
|
+
suggestion: "Ensure no application code directly references the partition before detaching.",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "DISABLE_TRIGGER",
|
|
182
|
+
severity: "HIGH",
|
|
183
|
+
pattern: /\bDISABLE\s+TRIGGER\b/i,
|
|
184
|
+
message: "DISABLE TRIGGER bypasses trigger-based validation — dirty data may enter the table.",
|
|
185
|
+
suggestion: "Re-enable the trigger immediately after the data operation and validate data integrity.",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "VACUUM_FULL",
|
|
189
|
+
severity: "MEDIUM",
|
|
190
|
+
pattern: /\bVACUUM\s+FULL\b/i,
|
|
191
|
+
message: "VACUUM FULL acquires an exclusive table lock for the full duration of the operation.",
|
|
192
|
+
suggestion: "Use regular VACUUM or pg_repack for online space reclamation.",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "DROP_DOMAIN",
|
|
196
|
+
severity: "MEDIUM",
|
|
197
|
+
pattern: /\bDROP\s+DOMAIN\b/i,
|
|
198
|
+
message: "DROP DOMAIN may break columns or functions that use this domain type.",
|
|
199
|
+
suggestion: "Ensure no tables or functions reference this domain before dropping it.",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "DROP_AGGREGATE",
|
|
203
|
+
severity: "MEDIUM",
|
|
204
|
+
pattern: /\bDROP\s+AGGREGATE\b/i,
|
|
205
|
+
message: "DROP AGGREGATE may break queries that use this aggregate function.",
|
|
206
|
+
suggestion: "Ensure no queries or views reference this aggregate before dropping it.",
|
|
207
|
+
},
|
|
208
|
+
// ── MySQL-specific rules ─────────────────────────────────────────────────
|
|
209
|
+
{
|
|
210
|
+
id: "MYSQL_DROP_DATABASE",
|
|
211
|
+
severity: "CRITICAL",
|
|
212
|
+
pattern: /\bDROP\s+(?:DATABASE|SCHEMA)\b/i,
|
|
213
|
+
message: "DROP DATABASE/SCHEMA is irreversible — all tables and data will be permanently lost.",
|
|
214
|
+
suggestion: "Back up the database and confirm with your team before dropping.",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "MYSQL_ALTER_TABLE_MODIFY_COLUMN",
|
|
218
|
+
severity: "HIGH",
|
|
219
|
+
pattern: /\bALTER\s+TABLE\s+\S+\s+MODIFY\s+(?:COLUMN\s+)?\S+/i,
|
|
220
|
+
message: "MODIFY COLUMN may fail if existing data cannot be cast to the new type (MySQL).",
|
|
221
|
+
suggestion: "Test the type change on a copy of the data first.",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "MYSQL_ALTER_TABLE_CHANGE",
|
|
225
|
+
severity: "HIGH",
|
|
226
|
+
pattern: /\bALTER\s+TABLE\s+\S+\s+CHANGE\s+(?:COLUMN\s+)?/i,
|
|
227
|
+
message: "CHANGE COLUMN renames and/or changes type — a breaking change for queries using the old name (MySQL).",
|
|
228
|
+
suggestion: "Update all application code before renaming the column.",
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
function sanitize(sql) {
|
|
232
|
+
return sql
|
|
233
|
+
.replace(/\$([^$]*)\$[\s\S]*?\$\1\$/g, "") // dollar-quoted bodies
|
|
234
|
+
.replace(/--[^\n]*/g, " ") // line comments
|
|
235
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ") // block comments
|
|
236
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, "''") // single-quoted strings
|
|
237
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, '""'); // double-quoted identifiers
|
|
238
|
+
}
|
|
239
|
+
function checkStatement(statement, lineNumber, file, disableRules = []) {
|
|
240
|
+
const issues = [];
|
|
241
|
+
const trimmed = statement.trim();
|
|
242
|
+
if (!trimmed)
|
|
243
|
+
return issues;
|
|
244
|
+
const sanitized = sanitize(trimmed);
|
|
245
|
+
for (const rule of RULES) {
|
|
246
|
+
if (disableRules.includes(rule.id))
|
|
247
|
+
continue;
|
|
248
|
+
if (rule.pattern.test(sanitized)) {
|
|
249
|
+
issues.push({
|
|
250
|
+
severity: rule.severity,
|
|
251
|
+
file,
|
|
252
|
+
line: lineNumber,
|
|
253
|
+
statement: trimmed.split("\n")[0].substring(0, 120),
|
|
254
|
+
message: rule.message,
|
|
255
|
+
suggestion: rule.suggestion,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return issues;
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/checker/rules.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const CONFIG_FILES = [".migrasaferc.json", ".migrasaferc", "migrasafe.config.json"];
|
|
10
|
+
function loadConfig(cwd = process.cwd()) {
|
|
11
|
+
for (const name of CONFIG_FILES) {
|
|
12
|
+
const filePath = path_1.default.join(cwd, name);
|
|
13
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs_1.default.readFileSync(filePath, "utf-8").replace(/^/, "");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
console.error(`Warning: failed to parse config file ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;;;AAWA,gCAaC;AAxBD,4CAAoB;AACpB,gDAAwB;AAQxB,MAAM,YAAY,GAAG,CAAC,mBAAmB,EAAE,cAAc,EAAE,uBAAuB,CAAC,CAAC;AAEpF,SAAgB,UAAU,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACpD,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACjE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,KAAK,CAAC,wCAAwC,QAAQ,EAAE,CAAC,CAAC;YACpE,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const checker_1 = require("./checker/checker");
|
|
11
|
+
const formatter_1 = require("./output/formatter");
|
|
12
|
+
const config_1 = require("./config");
|
|
13
|
+
const SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"];
|
|
14
|
+
const program = new commander_1.Command();
|
|
15
|
+
program
|
|
16
|
+
.name("migrasafe")
|
|
17
|
+
.description("Detect unsafe SQL migrations before deploying to production")
|
|
18
|
+
.version("1.0.0");
|
|
19
|
+
program
|
|
20
|
+
.command("check <target>")
|
|
21
|
+
.description("Check a SQL file or directory of migrations")
|
|
22
|
+
.option("--format <format>", "Output format: text or json", "text")
|
|
23
|
+
.option("--ignore <patterns...>", "Glob/regex patterns to ignore (e.g. --ignore seed_ test_)")
|
|
24
|
+
.option("--min-severity <level>", "Minimum severity to report: CRITICAL, HIGH, MEDIUM, LOW, INFO", "INFO")
|
|
25
|
+
.action((target, options) => {
|
|
26
|
+
const resolved = path_1.default.resolve(target);
|
|
27
|
+
if (!fs_1.default.existsSync(resolved)) {
|
|
28
|
+
console.error(`Error: path not found — ${resolved}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Load config file, then merge CLI flags (CLI takes precedence)
|
|
32
|
+
const config = (0, config_1.loadConfig)();
|
|
33
|
+
if (options.ignore)
|
|
34
|
+
config.ignore = [...(config.ignore ?? []), ...options.ignore];
|
|
35
|
+
const minSeverity = options.minSeverity.toUpperCase() ?? "INFO";
|
|
36
|
+
const minIndex = SEVERITY_ORDER.indexOf(minSeverity);
|
|
37
|
+
let results;
|
|
38
|
+
try {
|
|
39
|
+
const stat = fs_1.default.statSync(resolved);
|
|
40
|
+
const ignorePatterns = (config.ignore ?? []).map((p) => new RegExp(p));
|
|
41
|
+
const isIgnored = ignorePatterns.some((re) => re.test(resolved));
|
|
42
|
+
if (isIgnored) {
|
|
43
|
+
console.log(`Skipped (ignored): ${resolved}`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
results = stat.isDirectory()
|
|
47
|
+
? (0, checker_1.checkDirectory)(resolved, config)
|
|
48
|
+
: [(0, checker_1.checkFile)(resolved, config)];
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
console.error(`Error: ${msg}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Filter issues by min severity (always apply — default INFO keeps everything)
|
|
56
|
+
results = results.map((r) => ({
|
|
57
|
+
...r,
|
|
58
|
+
issues: r.issues.filter((i) => SEVERITY_ORDER.indexOf(i.severity) <= minIndex),
|
|
59
|
+
}));
|
|
60
|
+
const scanResult = (0, checker_1.buildScanResult)(results);
|
|
61
|
+
if (options.format === "json") {
|
|
62
|
+
console.log((0, formatter_1.formatJson)(scanResult));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log((0, formatter_1.formatText)(scanResult));
|
|
66
|
+
}
|
|
67
|
+
process.exit(scanResult.safe ? 0 : 1);
|
|
68
|
+
});
|
|
69
|
+
program
|
|
70
|
+
.command("install-hook")
|
|
71
|
+
.description("Install a pre-commit git hook that runs migrasafe on staged .sql files")
|
|
72
|
+
.action(() => {
|
|
73
|
+
const hookDir = path_1.default.join(process.cwd(), ".git", "hooks");
|
|
74
|
+
const hookPath = path_1.default.join(hookDir, "pre-commit");
|
|
75
|
+
if (!fs_1.default.existsSync(hookDir)) {
|
|
76
|
+
console.error("Error: .git/hooks directory not found. Are you in a git repository?");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const hookScript = `#!/bin/sh
|
|
80
|
+
# migrasafe pre-commit hook
|
|
81
|
+
STAGED_SQL=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.sql$')
|
|
82
|
+
if [ -z "$STAGED_SQL" ]; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
echo "migrasafe: checking staged SQL migrations..."
|
|
86
|
+
echo "$STAGED_SQL" | xargs npx migrasafe check
|
|
87
|
+
`;
|
|
88
|
+
if (fs_1.default.existsSync(hookPath)) {
|
|
89
|
+
const existing = fs_1.default.readFileSync(hookPath, "utf-8");
|
|
90
|
+
if (existing.includes("migrasafe")) {
|
|
91
|
+
console.log("migrasafe hook already installed.");
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
// Append to existing hook
|
|
95
|
+
fs_1.default.appendFileSync(hookPath, "\n" + hookScript);
|
|
96
|
+
console.log("✔ migrasafe hook appended to existing pre-commit hook.");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
fs_1.default.writeFileSync(hookPath, hookScript, { mode: 0o755 });
|
|
100
|
+
console.log("✔ migrasafe pre-commit hook installed at .git/hooks/pre-commit");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
program.parse();
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../../src/output/formatter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAY,MAAM,UAAU,CAAC;AAkBhD,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAkDrD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAyBrD"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatText = formatText;
|
|
7
|
+
exports.formatJson = formatJson;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const SEVERITY_COLOR = {
|
|
12
|
+
CRITICAL: chalk_1.default.bgRed.white.bold,
|
|
13
|
+
HIGH: chalk_1.default.red.bold,
|
|
14
|
+
MEDIUM: chalk_1.default.yellow.bold,
|
|
15
|
+
LOW: chalk_1.default.blue,
|
|
16
|
+
INFO: chalk_1.default.gray,
|
|
17
|
+
};
|
|
18
|
+
const SEVERITY_ICON = {
|
|
19
|
+
CRITICAL: "✖ CRITICAL",
|
|
20
|
+
HIGH: "⚠ HIGH ",
|
|
21
|
+
MEDIUM: "● MEDIUM ",
|
|
22
|
+
LOW: "○ LOW ",
|
|
23
|
+
INFO: "· INFO ",
|
|
24
|
+
};
|
|
25
|
+
function formatText(result) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const filesScanned = result.results.length;
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(chalk_1.default.bold(`Scanning ${filesScanned} file(s)...\n`));
|
|
30
|
+
let hasIssues = false;
|
|
31
|
+
for (const fileResult of result.results) {
|
|
32
|
+
if (fileResult.issues.length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
hasIssues = true;
|
|
35
|
+
lines.push(chalk_1.default.underline(path_1.default.relative(process.cwd(), fileResult.file)));
|
|
36
|
+
for (const issue of fileResult.issues) {
|
|
37
|
+
const color = SEVERITY_COLOR[issue.severity];
|
|
38
|
+
const icon = SEVERITY_ICON[issue.severity];
|
|
39
|
+
lines.push(` ${color(icon)} Line ${issue.line}`);
|
|
40
|
+
lines.push(` ${chalk_1.default.dim("Statement:")} ${chalk_1.default.white(issue.statement)}`);
|
|
41
|
+
lines.push(` ${chalk_1.default.dim("Problem :")} ${issue.message}`);
|
|
42
|
+
if (issue.suggestion) {
|
|
43
|
+
lines.push(` ${chalk_1.default.dim("Fix :")} ${chalk_1.default.cyan(issue.suggestion)}`);
|
|
44
|
+
}
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!hasIssues) {
|
|
49
|
+
lines.push(chalk_1.default.green("✔ All migrations are safe — no issues found.\n"));
|
|
50
|
+
}
|
|
51
|
+
lines.push(chalk_1.default.bold("── Summary ──────────────────────────────"));
|
|
52
|
+
if (result.criticalCount > 0)
|
|
53
|
+
lines.push(SEVERITY_COLOR.CRITICAL(` CRITICAL : ${result.criticalCount}`));
|
|
54
|
+
if (result.highCount > 0)
|
|
55
|
+
lines.push(SEVERITY_COLOR.HIGH(` HIGH : ${result.highCount}`));
|
|
56
|
+
if (result.mediumCount > 0)
|
|
57
|
+
lines.push(SEVERITY_COLOR.MEDIUM(` MEDIUM : ${result.mediumCount}`));
|
|
58
|
+
lines.push(` Total : ${result.totalIssues} issue(s) across ${filesScanned} file(s)`);
|
|
59
|
+
lines.push("");
|
|
60
|
+
if (result.safe) {
|
|
61
|
+
lines.push(chalk_1.default.green.bold("✔ SAFE — ready to deploy to production"));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push(chalk_1.default.red.bold("✖ UNSAFE — resolve all CRITICAL/HIGH issues before deploying"));
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
function formatJson(result) {
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
safe: result.safe,
|
|
72
|
+
summary: {
|
|
73
|
+
critical: result.criticalCount,
|
|
74
|
+
high: result.highCount,
|
|
75
|
+
medium: result.mediumCount,
|
|
76
|
+
total: result.totalIssues,
|
|
77
|
+
},
|
|
78
|
+
files: result.results.map((r) => ({
|
|
79
|
+
file: r.file,
|
|
80
|
+
issueCount: r.issues.length,
|
|
81
|
+
issues: r.issues.map((i) => ({
|
|
82
|
+
severity: i.severity,
|
|
83
|
+
line: i.line,
|
|
84
|
+
statement: i.statement,
|
|
85
|
+
message: i.message,
|
|
86
|
+
suggestion: i.suggestion,
|
|
87
|
+
})),
|
|
88
|
+
})),
|
|
89
|
+
}, null, 2);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../src/output/formatter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Severity = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO";
|
|
2
|
+
export interface Issue {
|
|
3
|
+
severity: Severity;
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
statement: string;
|
|
7
|
+
message: string;
|
|
8
|
+
suggestion?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface CheckResult {
|
|
11
|
+
file: string;
|
|
12
|
+
issues: Issue[];
|
|
13
|
+
}
|
|
14
|
+
export interface ScanResult {
|
|
15
|
+
results: CheckResult[];
|
|
16
|
+
totalIssues: number;
|
|
17
|
+
criticalCount: number;
|
|
18
|
+
highCount: number;
|
|
19
|
+
mediumCount: number;
|
|
20
|
+
safe: boolean;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvE,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "migrasafe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Detect unsafe SQL migrations before deploying to production",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"migrasafe": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["sql", "migration", "database", "safety", "cli", "devops", "postgresql", "flyway", "liquibase"],
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "febrifelis",
|
|
18
|
+
"url": "https://github.com/febrifelis"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/febrifelis/migrasafe.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/febrifelis/migrasafe#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/febrifelis/migrasafe/issues"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"type": "commonjs",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^26.0.1",
|
|
40
|
+
"tsx": "^4.22.4",
|
|
41
|
+
"typescript": "^6.0.3"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"chalk": "^5.6.2",
|
|
45
|
+
"commander": "^15.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|