@xubylele/schema-forge 1.10.1 → 1.12.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 +40 -2
- package/dist/api.d.ts +3 -47
- package/dist/api.js +247 -9
- package/dist/cli.js +402 -83
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ A modern CLI tool for database schema management with a clean DSL and automatic
|
|
|
14
14
|
- **Postgres/Supabase** - Currently supports PostgreSQL and Supabase
|
|
15
15
|
- **Constraint Diffing** - Detects UNIQUE and PRIMARY KEY changes with deterministic constraint names
|
|
16
16
|
- **Live PostgreSQL Introspection** - Extract normalized schema directly from `information_schema`
|
|
17
|
+
- **Policy (RLS) support** - Define Row Level Security policies in the DSL; invalid policies produce clear CLI errors during validation
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -210,12 +211,13 @@ Creates the necessary directory structure and configuration files.
|
|
|
210
211
|
Generate SQL migration from schema changes.
|
|
211
212
|
|
|
212
213
|
```bash
|
|
213
|
-
schema-forge generate [--name "migration description"] [--safe] [--force]
|
|
214
|
+
schema-forge generate [--name "migration description"] [--migration-format hyphen|underscore] [--safe] [--force]
|
|
214
215
|
```
|
|
215
216
|
|
|
216
217
|
**Options:**
|
|
217
218
|
|
|
218
219
|
- `--name` - Optional name for the migration (default: "migration")
|
|
220
|
+
- `--migration-format <format>` - Migration file name: `hyphen` (e.g. `20250315120000-add-users.sql`) or `underscore` (e.g. `20250315120000_add_users.sql`, Supabase CLI style). Overrides `migrationFileNameFormat` in config.
|
|
219
221
|
- `--safe` - Block execution if destructive operations are detected (exits with error)
|
|
220
222
|
- `--force` - Bypass safety checks and proceed with destructive operations (shows warning)
|
|
221
223
|
|
|
@@ -269,6 +271,20 @@ Behavior:
|
|
|
269
271
|
- Ignores unsupported SQL safely and prints warnings
|
|
270
272
|
- Writes a normalized SchemaForge DSL schema file
|
|
271
273
|
|
|
274
|
+
### `schema-forge config`
|
|
275
|
+
|
|
276
|
+
Update `schemaforge/config.json` settings without editing the file.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
schema-forge config migration-format <format>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Subcommands:**
|
|
283
|
+
|
|
284
|
+
- **`migration-format <format>`** – Set migration file name format: `hyphen` (e.g. `20250315120000-add-users.sql`) or `underscore` (e.g. `20250315120000_add_users.sql`, Supabase CLI style). Writes `migrationFileNameFormat` to config.
|
|
285
|
+
|
|
286
|
+
Requires an initialized project. Other config keys are left unchanged.
|
|
287
|
+
|
|
272
288
|
### `schema-forge validate`
|
|
273
289
|
|
|
274
290
|
Detect destructive or risky schema changes before generating/applying migrations.
|
|
@@ -309,6 +325,7 @@ Live `--json` output returns a structured `DriftReport`:
|
|
|
309
325
|
|
|
310
326
|
Validation checks include:
|
|
311
327
|
|
|
328
|
+
- **Policy validation** – Each policy must reference an existing table, use a valid command (`select` / `insert` / `update` / `delete`), and have at least one of `using` or `with check`. Invalid policies cause validation to fail with a clear error (exit code 1).
|
|
312
329
|
- Dropped tables (`DROP_TABLE`, error)
|
|
313
330
|
- Dropped columns (`DROP_COLUMN`, error)
|
|
314
331
|
- Column type changes (`ALTER_COLUMN_TYPE`, warning/error based on compatibility heuristics)
|
|
@@ -551,6 +568,26 @@ table table_name {
|
|
|
551
568
|
- `default <value>` - Default value (e.g., `default now()`, `default false`, `default 0`)
|
|
552
569
|
- `fk <table>.<column>` - Foreign key reference (e.g., `fk users.id`)
|
|
553
570
|
|
|
571
|
+
### Policies (RLS)
|
|
572
|
+
|
|
573
|
+
Row Level Security policies are declared at the top level (the table must be defined first). Each policy must have a `for` command and at least one of `using` or `with check`.
|
|
574
|
+
|
|
575
|
+
```sql
|
|
576
|
+
table users {
|
|
577
|
+
id uuid pk
|
|
578
|
+
email text unique
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
policy "Users can read themselves" on users
|
|
582
|
+
for select
|
|
583
|
+
using auth.uid() = id
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
- **First line:** `policy "<name>" on <table>`
|
|
587
|
+
- **Continuation:** `for <command>` (required; one of `select`, `insert`, `update`, `delete`), optional `using <expr>`, optional `with check <expr>`
|
|
588
|
+
|
|
589
|
+
Invalid policies (missing table, invalid command, or no expressions) fail `schema-forge validate` with a clear error message.
|
|
590
|
+
|
|
554
591
|
### Examples
|
|
555
592
|
|
|
556
593
|
#### Simple table
|
|
@@ -626,7 +663,8 @@ The `schemaforge/config.json` file contains project configuration. The `provider
|
|
|
626
663
|
```
|
|
627
664
|
|
|
628
665
|
- **postgres**: `provider: "postgres"`, `outputDir: "migrations"`.
|
|
629
|
-
- **supabase**: `provider: "supabase"`, `outputDir: "supabase/migrations"`.
|
|
666
|
+
- **supabase**: `provider: "supabase"`, `outputDir: "supabase/migrations"`. Init also sets `migrationFileNameFormat: "hyphen"` so migrations are named `timestamp-name.sql` (Supabase CLI uses `timestamp_name.sql` by default; you can set `migrationFileNameFormat: "underscore"` in config or use `--migration-format underscore` to match).
|
|
667
|
+
- **migrationFileNameFormat** (optional): `"hyphen"` (default) or `"underscore"`. Controls generated migration file names: `YYYYMMDDHHmmss-name.sql` vs `YYYYMMDDHHmmss_name.sql`. You can set it via **`schema-forge config migration-format <hyphen|underscore>`** instead of editing the file.
|
|
630
668
|
|
|
631
669
|
## Supported Databases
|
|
632
670
|
|
package/dist/api.d.ts
CHANGED
|
@@ -11,10 +11,13 @@ interface DoctorOptions {
|
|
|
11
11
|
schema?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
type MigrationFileNameFormat = 'hyphen' | 'underscore';
|
|
15
|
+
|
|
14
16
|
interface GenerateOptions {
|
|
15
17
|
name?: string;
|
|
16
18
|
safe?: boolean;
|
|
17
19
|
force?: boolean;
|
|
20
|
+
migrationFormat?: MigrationFileNameFormat;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
interface ImportOptions {
|
|
@@ -39,69 +42,22 @@ interface ValidateOptions {
|
|
|
39
42
|
force?: boolean;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
/**
|
|
43
|
-
* Exit codes used throughout the CLI for deterministic behavior
|
|
44
|
-
*
|
|
45
|
-
* @see SF-106 Standardize Exit Codes
|
|
46
|
-
*/
|
|
47
45
|
declare const EXIT_CODES: {
|
|
48
|
-
/** Successful operation */
|
|
49
46
|
readonly SUCCESS: 0;
|
|
50
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
51
47
|
readonly VALIDATION_ERROR: 1;
|
|
52
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
53
48
|
readonly DRIFT_DETECTED: 2;
|
|
54
|
-
/** Destructive operation detected in CI environment without --force */
|
|
55
49
|
readonly CI_DESTRUCTIVE: 3;
|
|
56
50
|
};
|
|
57
51
|
|
|
58
|
-
/**
|
|
59
|
-
* Programmatic API for Schema Forge.
|
|
60
|
-
* Use this entrypoint when integrating from Node (e.g. scripts, GitHub Actions)
|
|
61
|
-
* instead of invoking the CLI via shell.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* const { generate, EXIT_CODES } = require('@xubylele/schema-forge/api');
|
|
65
|
-
* const result = await generate({ name: 'MyMigration' });
|
|
66
|
-
* if (result.exitCode !== EXIT_CODES.SUCCESS) process.exit(result.exitCode);
|
|
67
|
-
*/
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Result of a programmatic command run. Exit codes match the CLI contract.
|
|
71
|
-
* @see docs/exit-codes.json
|
|
72
|
-
*/
|
|
73
52
|
interface RunResult {
|
|
74
53
|
exitCode: number;
|
|
75
54
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Initialize a new schema project in the current directory.
|
|
78
|
-
* @param options.provider - Database provider: 'postgres' (default) or 'supabase'. Supabase uses supabase/migrations for output.
|
|
79
|
-
*/
|
|
80
55
|
declare function init(options?: InitOptions): Promise<RunResult>;
|
|
81
|
-
/**
|
|
82
|
-
* Generate SQL migration from schema files.
|
|
83
|
-
*/
|
|
84
56
|
declare function generate(options?: GenerateOptions): Promise<RunResult>;
|
|
85
|
-
/**
|
|
86
|
-
* Compare two schema versions and generate migration SQL (optionally against live DB).
|
|
87
|
-
*/
|
|
88
57
|
declare function diff(options?: DiffOptions): Promise<RunResult>;
|
|
89
|
-
/**
|
|
90
|
-
* Check live database drift against state.
|
|
91
|
-
*/
|
|
92
58
|
declare function doctor(options?: DoctorOptions): Promise<RunResult>;
|
|
93
|
-
/**
|
|
94
|
-
* Validate schema and optionally check for destructive changes or live drift.
|
|
95
|
-
*/
|
|
96
59
|
declare function validate(options?: ValidateOptions): Promise<RunResult>;
|
|
97
|
-
/**
|
|
98
|
-
* Extract normalized live schema from PostgreSQL.
|
|
99
|
-
*/
|
|
100
60
|
declare function introspect(options?: IntrospectOptions): Promise<RunResult>;
|
|
101
|
-
/**
|
|
102
|
-
* Import schema from SQL migrations.
|
|
103
|
-
* @param inputPath - Path to .sql file or migrations directory
|
|
104
|
-
*/
|
|
105
61
|
declare function importSchema(inputPath: string, options?: ImportOptions): Promise<RunResult>;
|
|
106
62
|
|
|
107
63
|
export { type DiffOptions, type DoctorOptions, EXIT_CODES, type GenerateOptions, type ImportOptions, type InitOptions, type IntrospectOptions, type RunResult, type ValidateOptions, diff, doctor, generate, importSchema, init, introspect, validate };
|
package/dist/api.js
CHANGED
|
@@ -34,6 +34,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
function parseSchema(source) {
|
|
35
35
|
const lines = source.split("\n");
|
|
36
36
|
const tables = {};
|
|
37
|
+
const policyList = [];
|
|
37
38
|
let currentLine = 0;
|
|
38
39
|
const validBaseColumnTypes = /* @__PURE__ */ new Set([
|
|
39
40
|
"uuid",
|
|
@@ -258,6 +259,67 @@ function parseSchema(source) {
|
|
|
258
259
|
}
|
|
259
260
|
return column;
|
|
260
261
|
}
|
|
262
|
+
function parsePolicyBlock(startLine) {
|
|
263
|
+
const firstLine = cleanLine(lines[startLine]);
|
|
264
|
+
const declMatch = firstLine.match(/^policy\s+"([^"]*)"\s+on\s+(\w+)\s*$/);
|
|
265
|
+
if (!declMatch) {
|
|
266
|
+
throw new Error(`Line ${startLine + 1}: Invalid policy declaration. Expected: policy "<name>" on <table>`);
|
|
267
|
+
}
|
|
268
|
+
const policyName = declMatch[1];
|
|
269
|
+
const tableIdent = declMatch[2];
|
|
270
|
+
validateIdentifier(tableIdent, startLine + 1, "table");
|
|
271
|
+
let command;
|
|
272
|
+
let using;
|
|
273
|
+
let withCheck;
|
|
274
|
+
let lineIdx = startLine + 1;
|
|
275
|
+
while (lineIdx < lines.length) {
|
|
276
|
+
const cleaned = cleanLine(lines[lineIdx]);
|
|
277
|
+
if (!cleaned) {
|
|
278
|
+
lineIdx++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (cleaned.startsWith("for ")) {
|
|
282
|
+
const cmd = cleaned.slice(4).trim().toLowerCase();
|
|
283
|
+
if (!POLICY_COMMANDS.includes(cmd)) {
|
|
284
|
+
throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, or delete`);
|
|
285
|
+
}
|
|
286
|
+
if (command !== void 0) {
|
|
287
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'for' in policy`);
|
|
288
|
+
}
|
|
289
|
+
command = cmd;
|
|
290
|
+
lineIdx++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (cleaned.startsWith("using ")) {
|
|
294
|
+
if (using !== void 0) {
|
|
295
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'using' in policy`);
|
|
296
|
+
}
|
|
297
|
+
using = cleaned.slice(6).trim();
|
|
298
|
+
lineIdx++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (cleaned.startsWith("with check ")) {
|
|
302
|
+
if (withCheck !== void 0) {
|
|
303
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'with check' in policy`);
|
|
304
|
+
}
|
|
305
|
+
withCheck = cleaned.slice(11).trim();
|
|
306
|
+
lineIdx++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (command === void 0) {
|
|
312
|
+
throw new Error(`Line ${startLine + 1}: Policy is missing 'for <command>'`);
|
|
313
|
+
}
|
|
314
|
+
const policy = {
|
|
315
|
+
name: policyName,
|
|
316
|
+
table: tableIdent,
|
|
317
|
+
command,
|
|
318
|
+
...using !== void 0 && { using },
|
|
319
|
+
...withCheck !== void 0 && { withCheck }
|
|
320
|
+
};
|
|
321
|
+
return { policy, nextLineIndex: lineIdx };
|
|
322
|
+
}
|
|
261
323
|
function parseTableBlock(startLine) {
|
|
262
324
|
const firstLine = cleanLine(lines[startLine]);
|
|
263
325
|
const match = firstLine.match(/^table\s+(\w+)\s*\{\s*$/);
|
|
@@ -301,6 +363,7 @@ function parseSchema(source) {
|
|
|
301
363
|
};
|
|
302
364
|
return lineIdx;
|
|
303
365
|
}
|
|
366
|
+
const policyDeclRegex = /^policy\s+"([^"]*)"\s+on\s+\w+/;
|
|
304
367
|
while (currentLine < lines.length) {
|
|
305
368
|
const cleaned = cleanLine(lines[currentLine]);
|
|
306
369
|
if (!cleaned) {
|
|
@@ -309,16 +372,31 @@ function parseSchema(source) {
|
|
|
309
372
|
}
|
|
310
373
|
if (cleaned.startsWith("table ")) {
|
|
311
374
|
currentLine = parseTableBlock(currentLine);
|
|
375
|
+
currentLine++;
|
|
376
|
+
} else if (policyDeclRegex.test(cleaned)) {
|
|
377
|
+
const { policy, nextLineIndex } = parsePolicyBlock(currentLine);
|
|
378
|
+
policyList.push({ policy, startLine: currentLine + 1 });
|
|
379
|
+
currentLine = nextLineIndex;
|
|
312
380
|
} else {
|
|
313
|
-
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table definition.`);
|
|
381
|
+
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table or policy definition.`);
|
|
314
382
|
}
|
|
315
|
-
|
|
383
|
+
}
|
|
384
|
+
for (const { policy, startLine } of policyList) {
|
|
385
|
+
if (!tables[policy.table]) {
|
|
386
|
+
throw new Error(`Line ${startLine}: Policy "${policy.name}" references undefined table "${policy.table}"`);
|
|
387
|
+
}
|
|
388
|
+
if (!tables[policy.table].policies) {
|
|
389
|
+
tables[policy.table].policies = [];
|
|
390
|
+
}
|
|
391
|
+
tables[policy.table].policies.push(policy);
|
|
316
392
|
}
|
|
317
393
|
return { tables };
|
|
318
394
|
}
|
|
395
|
+
var POLICY_COMMANDS;
|
|
319
396
|
var init_parser = __esm({
|
|
320
397
|
"node_modules/@xubylele/schema-forge-core/dist/core/parser.js"() {
|
|
321
398
|
"use strict";
|
|
399
|
+
POLICY_COMMANDS = ["select", "insert", "update", "delete"];
|
|
322
400
|
}
|
|
323
401
|
});
|
|
324
402
|
|
|
@@ -498,6 +576,18 @@ function resolveSchemaPrimaryKey(table) {
|
|
|
498
576
|
function normalizeNullable(nullable) {
|
|
499
577
|
return nullable ?? true;
|
|
500
578
|
}
|
|
579
|
+
function normalizePolicyExpression(s) {
|
|
580
|
+
return (s ?? "").trim().replace(/\s+/g, " ");
|
|
581
|
+
}
|
|
582
|
+
function policyEquals(oldP, newP) {
|
|
583
|
+
if (oldP.command !== newP.command)
|
|
584
|
+
return false;
|
|
585
|
+
if (normalizePolicyExpression(oldP.using) !== normalizePolicyExpression(newP.using))
|
|
586
|
+
return false;
|
|
587
|
+
if (normalizePolicyExpression(oldP.withCheck) !== normalizePolicyExpression(newP.withCheck))
|
|
588
|
+
return false;
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
501
591
|
function diffSchemas(oldState, newSchema) {
|
|
502
592
|
const operations = [];
|
|
503
593
|
const oldTableNames = getTableNamesFromState(oldState);
|
|
@@ -674,6 +764,37 @@ function diffSchemas(oldState, newSchema) {
|
|
|
674
764
|
}
|
|
675
765
|
}
|
|
676
766
|
}
|
|
767
|
+
const oldPolicyNamesByTable = (t) => new Set(Object.keys(t.policies ?? {}));
|
|
768
|
+
const newPolicyListByTable = (t) => t.policies ?? [];
|
|
769
|
+
for (const tableName of commonTableNames) {
|
|
770
|
+
const newTable = newSchema.tables[tableName];
|
|
771
|
+
const oldTable = oldState.tables[tableName];
|
|
772
|
+
if (!newTable || !oldTable)
|
|
773
|
+
continue;
|
|
774
|
+
const oldPolicyNames = oldPolicyNamesByTable(oldTable);
|
|
775
|
+
const newPolicies = newPolicyListByTable(newTable);
|
|
776
|
+
const newPolicyNames = new Set(newPolicies.map((p) => p.name));
|
|
777
|
+
const sortedNewPolicyNames = Array.from(newPolicyNames).sort((a, b) => a.localeCompare(b));
|
|
778
|
+
const sortedOldPolicyNames = Array.from(oldPolicyNames).sort((a, b) => a.localeCompare(b));
|
|
779
|
+
for (const policyName of sortedNewPolicyNames) {
|
|
780
|
+
const policy = newPolicies.find((p) => p.name === policyName);
|
|
781
|
+
if (!policy)
|
|
782
|
+
continue;
|
|
783
|
+
if (!oldPolicyNames.has(policyName)) {
|
|
784
|
+
operations.push({ kind: "create_policy", tableName, policy });
|
|
785
|
+
} else {
|
|
786
|
+
const oldP = oldTable.policies?.[policyName];
|
|
787
|
+
if (oldP && !policyEquals(oldP, policy)) {
|
|
788
|
+
operations.push({ kind: "modify_policy", tableName, policyName, policy });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const policyName of sortedOldPolicyNames) {
|
|
793
|
+
if (!newPolicyNames.has(policyName)) {
|
|
794
|
+
operations.push({ kind: "drop_policy", tableName, policyName });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
677
798
|
for (const tableName of sortedOldTableNames) {
|
|
678
799
|
if (!newTableNames.has(tableName)) {
|
|
679
800
|
operations.push({
|
|
@@ -774,6 +895,7 @@ function validateSchema(schema) {
|
|
|
774
895
|
const table = schema.tables[tableName];
|
|
775
896
|
validateTableColumns(tableName, table, schema.tables);
|
|
776
897
|
}
|
|
898
|
+
validatePolicies(schema);
|
|
777
899
|
}
|
|
778
900
|
function validateDuplicateTables(schema) {
|
|
779
901
|
const tableNames = Object.keys(schema.tables);
|
|
@@ -829,10 +951,36 @@ function validateTableColumns(tableName, table, allTables) {
|
|
|
829
951
|
}
|
|
830
952
|
}
|
|
831
953
|
}
|
|
832
|
-
|
|
954
|
+
function validatePolicies(schema) {
|
|
955
|
+
for (const tableName in schema.tables) {
|
|
956
|
+
const table = schema.tables[tableName];
|
|
957
|
+
if (!table.policies?.length)
|
|
958
|
+
continue;
|
|
959
|
+
for (const policy of table.policies) {
|
|
960
|
+
if (!schema.tables[policy.table]) {
|
|
961
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": referenced table "${policy.table}" does not exist`);
|
|
962
|
+
}
|
|
963
|
+
if (!VALID_POLICY_COMMANDS.includes(policy.command)) {
|
|
964
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": invalid command "${policy.command}". Expected: select, insert, update, or delete`);
|
|
965
|
+
}
|
|
966
|
+
const hasUsing = policy.using !== void 0 && String(policy.using).trim() !== "";
|
|
967
|
+
const hasWithCheck = policy.withCheck !== void 0 && String(policy.withCheck).trim() !== "";
|
|
968
|
+
if (!hasUsing && !hasWithCheck) {
|
|
969
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": must have at least one of "using" or "with check"`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
var VALID_POLICY_COMMANDS, VALID_BASE_COLUMN_TYPES;
|
|
833
975
|
var init_validator = __esm({
|
|
834
976
|
"node_modules/@xubylele/schema-forge-core/dist/core/validator.js"() {
|
|
835
977
|
"use strict";
|
|
978
|
+
VALID_POLICY_COMMANDS = [
|
|
979
|
+
"select",
|
|
980
|
+
"insert",
|
|
981
|
+
"update",
|
|
982
|
+
"delete"
|
|
983
|
+
];
|
|
836
984
|
VALID_BASE_COLUMN_TYPES = [
|
|
837
985
|
"uuid",
|
|
838
986
|
"varchar",
|
|
@@ -926,6 +1074,12 @@ function classifyOperation(operation) {
|
|
|
926
1074
|
return "DESTRUCTIVE";
|
|
927
1075
|
case "add_primary_key_constraint":
|
|
928
1076
|
return "SAFE";
|
|
1077
|
+
case "create_policy":
|
|
1078
|
+
return "SAFE";
|
|
1079
|
+
case "drop_policy":
|
|
1080
|
+
return "DESTRUCTIVE";
|
|
1081
|
+
case "modify_policy":
|
|
1082
|
+
return "WARNING";
|
|
929
1083
|
default:
|
|
930
1084
|
const _exhaustive = operation;
|
|
931
1085
|
return _exhaustive;
|
|
@@ -1048,6 +1202,22 @@ function checkOperationSafety(operation) {
|
|
|
1048
1202
|
message: "Primary key constraint removed",
|
|
1049
1203
|
operationKind: operation.kind
|
|
1050
1204
|
};
|
|
1205
|
+
case "drop_policy":
|
|
1206
|
+
return {
|
|
1207
|
+
safetyLevel,
|
|
1208
|
+
code: "DROP_POLICY",
|
|
1209
|
+
table: operation.tableName,
|
|
1210
|
+
message: "Policy removed",
|
|
1211
|
+
operationKind: operation.kind
|
|
1212
|
+
};
|
|
1213
|
+
case "modify_policy":
|
|
1214
|
+
return {
|
|
1215
|
+
safetyLevel,
|
|
1216
|
+
code: "MODIFY_POLICY",
|
|
1217
|
+
table: operation.tableName,
|
|
1218
|
+
message: "Policy expression changed",
|
|
1219
|
+
operationKind: operation.kind
|
|
1220
|
+
};
|
|
1051
1221
|
default:
|
|
1052
1222
|
return null;
|
|
1053
1223
|
}
|
|
@@ -1222,9 +1392,21 @@ async function schemaToState(schema) {
|
|
|
1222
1392
|
...column.foreignKey !== void 0 && { foreignKey: column.foreignKey }
|
|
1223
1393
|
};
|
|
1224
1394
|
}
|
|
1395
|
+
let policies;
|
|
1396
|
+
if (table.policies?.length) {
|
|
1397
|
+
policies = {};
|
|
1398
|
+
for (const p of table.policies) {
|
|
1399
|
+
policies[p.name] = {
|
|
1400
|
+
command: p.command,
|
|
1401
|
+
...p.using !== void 0 && { using: p.using },
|
|
1402
|
+
...p.withCheck !== void 0 && { withCheck: p.withCheck }
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1225
1406
|
tables[tableName] = {
|
|
1226
1407
|
columns,
|
|
1227
|
-
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn }
|
|
1408
|
+
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn },
|
|
1409
|
+
...policies !== void 0 && { policies }
|
|
1228
1410
|
};
|
|
1229
1411
|
}
|
|
1230
1412
|
return {
|
|
@@ -1259,9 +1441,20 @@ var init_state_manager = __esm({
|
|
|
1259
1441
|
});
|
|
1260
1442
|
|
|
1261
1443
|
// node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js
|
|
1444
|
+
function generateEnableRls(tableName) {
|
|
1445
|
+
return `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`;
|
|
1446
|
+
}
|
|
1262
1447
|
function generateSql(diff2, provider, sqlConfig) {
|
|
1263
1448
|
const statements = [];
|
|
1449
|
+
const enabledRlsTables = /* @__PURE__ */ new Set();
|
|
1264
1450
|
for (const operation of diff2.operations) {
|
|
1451
|
+
if (operation.kind === "create_policy" || operation.kind === "modify_policy") {
|
|
1452
|
+
const tableName = operation.tableName;
|
|
1453
|
+
if (!enabledRlsTables.has(tableName)) {
|
|
1454
|
+
statements.push(generateEnableRls(tableName));
|
|
1455
|
+
enabledRlsTables.add(tableName);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1265
1458
|
const sql = generateOperation(operation, provider, sqlConfig);
|
|
1266
1459
|
if (sql) {
|
|
1267
1460
|
statements.push(sql);
|
|
@@ -1291,6 +1484,12 @@ function generateOperation(operation, provider, sqlConfig) {
|
|
|
1291
1484
|
return generateDropPrimaryKeyConstraint(operation.tableName);
|
|
1292
1485
|
case "add_primary_key_constraint":
|
|
1293
1486
|
return generateAddPrimaryKeyConstraint(operation.tableName, operation.columnName);
|
|
1487
|
+
case "create_policy":
|
|
1488
|
+
return generateCreatePolicy(operation.tableName, operation.policy);
|
|
1489
|
+
case "drop_policy":
|
|
1490
|
+
return generateDropPolicy(operation.tableName, operation.policyName);
|
|
1491
|
+
case "modify_policy":
|
|
1492
|
+
return generateModifyPolicy(operation.tableName, operation.policyName, operation.policy);
|
|
1294
1493
|
}
|
|
1295
1494
|
}
|
|
1296
1495
|
function generateCreateTable(table, provider, sqlConfig) {
|
|
@@ -1371,6 +1570,25 @@ function generateAlterColumnNullability(tableName, columnName, toNullable) {
|
|
|
1371
1570
|
}
|
|
1372
1571
|
return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET NOT NULL;`;
|
|
1373
1572
|
}
|
|
1573
|
+
function generateCreatePolicy(tableName, policy) {
|
|
1574
|
+
const command = policy.command.toUpperCase();
|
|
1575
|
+
const parts = [`CREATE POLICY "${policy.name}" ON ${tableName} FOR ${command}`];
|
|
1576
|
+
if (policy.using !== void 0 && policy.using !== "") {
|
|
1577
|
+
parts.push(`USING (${policy.using})`);
|
|
1578
|
+
}
|
|
1579
|
+
if (policy.withCheck !== void 0 && policy.withCheck !== "") {
|
|
1580
|
+
parts.push(`WITH CHECK (${policy.withCheck})`);
|
|
1581
|
+
}
|
|
1582
|
+
return parts.join(" ") + ";";
|
|
1583
|
+
}
|
|
1584
|
+
function generateDropPolicy(tableName, policyName) {
|
|
1585
|
+
return `DROP POLICY "${policyName}" ON ${tableName};`;
|
|
1586
|
+
}
|
|
1587
|
+
function generateModifyPolicy(tableName, policyName, policy) {
|
|
1588
|
+
const drop = generateDropPolicy(tableName, policyName);
|
|
1589
|
+
const create = generateCreatePolicy(tableName, policy);
|
|
1590
|
+
return drop + "\n\n" + create;
|
|
1591
|
+
}
|
|
1374
1592
|
var init_sql_generator = __esm({
|
|
1375
1593
|
"node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js"() {
|
|
1376
1594
|
"use strict";
|
|
@@ -2941,13 +3159,9 @@ async function withPostgresQueryExecutor(connectionString, run) {
|
|
|
2941
3159
|
|
|
2942
3160
|
// src/utils/exitCodes.ts
|
|
2943
3161
|
var EXIT_CODES = {
|
|
2944
|
-
/** Successful operation */
|
|
2945
3162
|
SUCCESS: 0,
|
|
2946
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
2947
3163
|
VALIDATION_ERROR: 1,
|
|
2948
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
2949
3164
|
DRIFT_DETECTED: 2,
|
|
2950
|
-
/** Destructive operation detected in CI environment without --force */
|
|
2951
3165
|
CI_DESTRUCTIVE: 3
|
|
2952
3166
|
};
|
|
2953
3167
|
function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
|
|
@@ -3241,6 +3455,10 @@ function nowTimestamp2() {
|
|
|
3241
3455
|
function slugifyName2(name) {
|
|
3242
3456
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "migration";
|
|
3243
3457
|
}
|
|
3458
|
+
function migrationFileName(timestamp, slug, format = "hyphen") {
|
|
3459
|
+
const sep = format === "underscore" ? "_" : "-";
|
|
3460
|
+
return `${timestamp}${sep}${slug}.sql`;
|
|
3461
|
+
}
|
|
3244
3462
|
|
|
3245
3463
|
// src/commands/generate.ts
|
|
3246
3464
|
var REQUIRED_CONFIG_FIELDS = [
|
|
@@ -3248,6 +3466,19 @@ var REQUIRED_CONFIG_FIELDS = [
|
|
|
3248
3466
|
"stateFile",
|
|
3249
3467
|
"outputDir"
|
|
3250
3468
|
];
|
|
3469
|
+
var MIGRATION_FORMATS = ["hyphen", "underscore"];
|
|
3470
|
+
function resolveMigrationFormat(cliFormat, configFormat) {
|
|
3471
|
+
if (cliFormat !== void 0 && cliFormat !== "") {
|
|
3472
|
+
const normalized = cliFormat.trim().toLowerCase();
|
|
3473
|
+
if (MIGRATION_FORMATS.includes(normalized)) {
|
|
3474
|
+
return normalized;
|
|
3475
|
+
}
|
|
3476
|
+
throw new Error(
|
|
3477
|
+
`Invalid --migration-format "${cliFormat}". Allowed: ${MIGRATION_FORMATS.join(", ")}.`
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
return configFormat ?? "hyphen";
|
|
3481
|
+
}
|
|
3251
3482
|
function resolveConfigPath3(root, targetPath) {
|
|
3252
3483
|
return import_path9.default.isAbsolute(targetPath) ? targetPath : import_path9.default.join(root, targetPath);
|
|
3253
3484
|
}
|
|
@@ -3328,7 +3559,11 @@ Remove --safe flag or modify schema to avoid destructive changes.`
|
|
|
3328
3559
|
const sql = await generateSql2(diff2, provider, config.sql);
|
|
3329
3560
|
const timestamp = nowTimestamp2();
|
|
3330
3561
|
const slug = slugifyName2(options.name ?? "migration");
|
|
3331
|
-
const
|
|
3562
|
+
const format = resolveMigrationFormat(
|
|
3563
|
+
options.migrationFormat,
|
|
3564
|
+
config.migrationFileNameFormat
|
|
3565
|
+
);
|
|
3566
|
+
const fileName = migrationFileName(timestamp, slug, format);
|
|
3332
3567
|
await ensureDir(outputDir);
|
|
3333
3568
|
const migrationPath = import_path9.default.join(outputDir, fileName);
|
|
3334
3569
|
await writeTextFile(migrationPath, sql + "\n");
|
|
@@ -3464,6 +3699,9 @@ table users {
|
|
|
3464
3699
|
timestampDefault: "now()"
|
|
3465
3700
|
}
|
|
3466
3701
|
};
|
|
3702
|
+
if (provider === "supabase") {
|
|
3703
|
+
config.migrationFileNameFormat = "hyphen";
|
|
3704
|
+
}
|
|
3467
3705
|
await writeJsonFile(configPath, config);
|
|
3468
3706
|
success(`Created ${configPath}`);
|
|
3469
3707
|
const state = {
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
34
34
|
function parseSchema(source) {
|
|
35
35
|
const lines = source.split("\n");
|
|
36
36
|
const tables = {};
|
|
37
|
+
const policyList = [];
|
|
37
38
|
let currentLine = 0;
|
|
38
39
|
const validBaseColumnTypes = /* @__PURE__ */ new Set([
|
|
39
40
|
"uuid",
|
|
@@ -258,6 +259,67 @@ function parseSchema(source) {
|
|
|
258
259
|
}
|
|
259
260
|
return column;
|
|
260
261
|
}
|
|
262
|
+
function parsePolicyBlock(startLine) {
|
|
263
|
+
const firstLine = cleanLine(lines[startLine]);
|
|
264
|
+
const declMatch = firstLine.match(/^policy\s+"([^"]*)"\s+on\s+(\w+)\s*$/);
|
|
265
|
+
if (!declMatch) {
|
|
266
|
+
throw new Error(`Line ${startLine + 1}: Invalid policy declaration. Expected: policy "<name>" on <table>`);
|
|
267
|
+
}
|
|
268
|
+
const policyName = declMatch[1];
|
|
269
|
+
const tableIdent = declMatch[2];
|
|
270
|
+
validateIdentifier(tableIdent, startLine + 1, "table");
|
|
271
|
+
let command;
|
|
272
|
+
let using;
|
|
273
|
+
let withCheck;
|
|
274
|
+
let lineIdx = startLine + 1;
|
|
275
|
+
while (lineIdx < lines.length) {
|
|
276
|
+
const cleaned = cleanLine(lines[lineIdx]);
|
|
277
|
+
if (!cleaned) {
|
|
278
|
+
lineIdx++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (cleaned.startsWith("for ")) {
|
|
282
|
+
const cmd = cleaned.slice(4).trim().toLowerCase();
|
|
283
|
+
if (!POLICY_COMMANDS.includes(cmd)) {
|
|
284
|
+
throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, or delete`);
|
|
285
|
+
}
|
|
286
|
+
if (command !== void 0) {
|
|
287
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'for' in policy`);
|
|
288
|
+
}
|
|
289
|
+
command = cmd;
|
|
290
|
+
lineIdx++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (cleaned.startsWith("using ")) {
|
|
294
|
+
if (using !== void 0) {
|
|
295
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'using' in policy`);
|
|
296
|
+
}
|
|
297
|
+
using = cleaned.slice(6).trim();
|
|
298
|
+
lineIdx++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (cleaned.startsWith("with check ")) {
|
|
302
|
+
if (withCheck !== void 0) {
|
|
303
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'with check' in policy`);
|
|
304
|
+
}
|
|
305
|
+
withCheck = cleaned.slice(11).trim();
|
|
306
|
+
lineIdx++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (command === void 0) {
|
|
312
|
+
throw new Error(`Line ${startLine + 1}: Policy is missing 'for <command>'`);
|
|
313
|
+
}
|
|
314
|
+
const policy = {
|
|
315
|
+
name: policyName,
|
|
316
|
+
table: tableIdent,
|
|
317
|
+
command,
|
|
318
|
+
...using !== void 0 && { using },
|
|
319
|
+
...withCheck !== void 0 && { withCheck }
|
|
320
|
+
};
|
|
321
|
+
return { policy, nextLineIndex: lineIdx };
|
|
322
|
+
}
|
|
261
323
|
function parseTableBlock(startLine) {
|
|
262
324
|
const firstLine = cleanLine(lines[startLine]);
|
|
263
325
|
const match = firstLine.match(/^table\s+(\w+)\s*\{\s*$/);
|
|
@@ -301,6 +363,7 @@ function parseSchema(source) {
|
|
|
301
363
|
};
|
|
302
364
|
return lineIdx;
|
|
303
365
|
}
|
|
366
|
+
const policyDeclRegex = /^policy\s+"([^"]*)"\s+on\s+\w+/;
|
|
304
367
|
while (currentLine < lines.length) {
|
|
305
368
|
const cleaned = cleanLine(lines[currentLine]);
|
|
306
369
|
if (!cleaned) {
|
|
@@ -309,16 +372,31 @@ function parseSchema(source) {
|
|
|
309
372
|
}
|
|
310
373
|
if (cleaned.startsWith("table ")) {
|
|
311
374
|
currentLine = parseTableBlock(currentLine);
|
|
375
|
+
currentLine++;
|
|
376
|
+
} else if (policyDeclRegex.test(cleaned)) {
|
|
377
|
+
const { policy, nextLineIndex } = parsePolicyBlock(currentLine);
|
|
378
|
+
policyList.push({ policy, startLine: currentLine + 1 });
|
|
379
|
+
currentLine = nextLineIndex;
|
|
312
380
|
} else {
|
|
313
|
-
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table definition.`);
|
|
381
|
+
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table or policy definition.`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const { policy, startLine } of policyList) {
|
|
385
|
+
if (!tables[policy.table]) {
|
|
386
|
+
throw new Error(`Line ${startLine}: Policy "${policy.name}" references undefined table "${policy.table}"`);
|
|
387
|
+
}
|
|
388
|
+
if (!tables[policy.table].policies) {
|
|
389
|
+
tables[policy.table].policies = [];
|
|
314
390
|
}
|
|
315
|
-
|
|
391
|
+
tables[policy.table].policies.push(policy);
|
|
316
392
|
}
|
|
317
393
|
return { tables };
|
|
318
394
|
}
|
|
395
|
+
var POLICY_COMMANDS;
|
|
319
396
|
var init_parser = __esm({
|
|
320
397
|
"node_modules/@xubylele/schema-forge-core/dist/core/parser.js"() {
|
|
321
398
|
"use strict";
|
|
399
|
+
POLICY_COMMANDS = ["select", "insert", "update", "delete"];
|
|
322
400
|
}
|
|
323
401
|
});
|
|
324
402
|
|
|
@@ -498,6 +576,18 @@ function resolveSchemaPrimaryKey(table) {
|
|
|
498
576
|
function normalizeNullable(nullable) {
|
|
499
577
|
return nullable ?? true;
|
|
500
578
|
}
|
|
579
|
+
function normalizePolicyExpression(s) {
|
|
580
|
+
return (s ?? "").trim().replace(/\s+/g, " ");
|
|
581
|
+
}
|
|
582
|
+
function policyEquals(oldP, newP) {
|
|
583
|
+
if (oldP.command !== newP.command)
|
|
584
|
+
return false;
|
|
585
|
+
if (normalizePolicyExpression(oldP.using) !== normalizePolicyExpression(newP.using))
|
|
586
|
+
return false;
|
|
587
|
+
if (normalizePolicyExpression(oldP.withCheck) !== normalizePolicyExpression(newP.withCheck))
|
|
588
|
+
return false;
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
501
591
|
function diffSchemas(oldState, newSchema) {
|
|
502
592
|
const operations = [];
|
|
503
593
|
const oldTableNames = getTableNamesFromState(oldState);
|
|
@@ -674,6 +764,37 @@ function diffSchemas(oldState, newSchema) {
|
|
|
674
764
|
}
|
|
675
765
|
}
|
|
676
766
|
}
|
|
767
|
+
const oldPolicyNamesByTable = (t) => new Set(Object.keys(t.policies ?? {}));
|
|
768
|
+
const newPolicyListByTable = (t) => t.policies ?? [];
|
|
769
|
+
for (const tableName of commonTableNames) {
|
|
770
|
+
const newTable = newSchema.tables[tableName];
|
|
771
|
+
const oldTable = oldState.tables[tableName];
|
|
772
|
+
if (!newTable || !oldTable)
|
|
773
|
+
continue;
|
|
774
|
+
const oldPolicyNames = oldPolicyNamesByTable(oldTable);
|
|
775
|
+
const newPolicies = newPolicyListByTable(newTable);
|
|
776
|
+
const newPolicyNames = new Set(newPolicies.map((p) => p.name));
|
|
777
|
+
const sortedNewPolicyNames = Array.from(newPolicyNames).sort((a, b) => a.localeCompare(b));
|
|
778
|
+
const sortedOldPolicyNames = Array.from(oldPolicyNames).sort((a, b) => a.localeCompare(b));
|
|
779
|
+
for (const policyName of sortedNewPolicyNames) {
|
|
780
|
+
const policy = newPolicies.find((p) => p.name === policyName);
|
|
781
|
+
if (!policy)
|
|
782
|
+
continue;
|
|
783
|
+
if (!oldPolicyNames.has(policyName)) {
|
|
784
|
+
operations.push({ kind: "create_policy", tableName, policy });
|
|
785
|
+
} else {
|
|
786
|
+
const oldP = oldTable.policies?.[policyName];
|
|
787
|
+
if (oldP && !policyEquals(oldP, policy)) {
|
|
788
|
+
operations.push({ kind: "modify_policy", tableName, policyName, policy });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const policyName of sortedOldPolicyNames) {
|
|
793
|
+
if (!newPolicyNames.has(policyName)) {
|
|
794
|
+
operations.push({ kind: "drop_policy", tableName, policyName });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
677
798
|
for (const tableName of sortedOldTableNames) {
|
|
678
799
|
if (!newTableNames.has(tableName)) {
|
|
679
800
|
operations.push({
|
|
@@ -774,6 +895,7 @@ function validateSchema(schema) {
|
|
|
774
895
|
const table = schema.tables[tableName];
|
|
775
896
|
validateTableColumns(tableName, table, schema.tables);
|
|
776
897
|
}
|
|
898
|
+
validatePolicies(schema);
|
|
777
899
|
}
|
|
778
900
|
function validateDuplicateTables(schema) {
|
|
779
901
|
const tableNames = Object.keys(schema.tables);
|
|
@@ -829,10 +951,36 @@ function validateTableColumns(tableName, table, allTables) {
|
|
|
829
951
|
}
|
|
830
952
|
}
|
|
831
953
|
}
|
|
832
|
-
|
|
954
|
+
function validatePolicies(schema) {
|
|
955
|
+
for (const tableName in schema.tables) {
|
|
956
|
+
const table = schema.tables[tableName];
|
|
957
|
+
if (!table.policies?.length)
|
|
958
|
+
continue;
|
|
959
|
+
for (const policy of table.policies) {
|
|
960
|
+
if (!schema.tables[policy.table]) {
|
|
961
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": referenced table "${policy.table}" does not exist`);
|
|
962
|
+
}
|
|
963
|
+
if (!VALID_POLICY_COMMANDS.includes(policy.command)) {
|
|
964
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": invalid command "${policy.command}". Expected: select, insert, update, or delete`);
|
|
965
|
+
}
|
|
966
|
+
const hasUsing = policy.using !== void 0 && String(policy.using).trim() !== "";
|
|
967
|
+
const hasWithCheck = policy.withCheck !== void 0 && String(policy.withCheck).trim() !== "";
|
|
968
|
+
if (!hasUsing && !hasWithCheck) {
|
|
969
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": must have at least one of "using" or "with check"`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
var VALID_POLICY_COMMANDS, VALID_BASE_COLUMN_TYPES;
|
|
833
975
|
var init_validator = __esm({
|
|
834
976
|
"node_modules/@xubylele/schema-forge-core/dist/core/validator.js"() {
|
|
835
977
|
"use strict";
|
|
978
|
+
VALID_POLICY_COMMANDS = [
|
|
979
|
+
"select",
|
|
980
|
+
"insert",
|
|
981
|
+
"update",
|
|
982
|
+
"delete"
|
|
983
|
+
];
|
|
836
984
|
VALID_BASE_COLUMN_TYPES = [
|
|
837
985
|
"uuid",
|
|
838
986
|
"varchar",
|
|
@@ -926,6 +1074,12 @@ function classifyOperation(operation) {
|
|
|
926
1074
|
return "DESTRUCTIVE";
|
|
927
1075
|
case "add_primary_key_constraint":
|
|
928
1076
|
return "SAFE";
|
|
1077
|
+
case "create_policy":
|
|
1078
|
+
return "SAFE";
|
|
1079
|
+
case "drop_policy":
|
|
1080
|
+
return "DESTRUCTIVE";
|
|
1081
|
+
case "modify_policy":
|
|
1082
|
+
return "WARNING";
|
|
929
1083
|
default:
|
|
930
1084
|
const _exhaustive = operation;
|
|
931
1085
|
return _exhaustive;
|
|
@@ -1048,6 +1202,22 @@ function checkOperationSafety(operation) {
|
|
|
1048
1202
|
message: "Primary key constraint removed",
|
|
1049
1203
|
operationKind: operation.kind
|
|
1050
1204
|
};
|
|
1205
|
+
case "drop_policy":
|
|
1206
|
+
return {
|
|
1207
|
+
safetyLevel,
|
|
1208
|
+
code: "DROP_POLICY",
|
|
1209
|
+
table: operation.tableName,
|
|
1210
|
+
message: "Policy removed",
|
|
1211
|
+
operationKind: operation.kind
|
|
1212
|
+
};
|
|
1213
|
+
case "modify_policy":
|
|
1214
|
+
return {
|
|
1215
|
+
safetyLevel,
|
|
1216
|
+
code: "MODIFY_POLICY",
|
|
1217
|
+
table: operation.tableName,
|
|
1218
|
+
message: "Policy expression changed",
|
|
1219
|
+
operationKind: operation.kind
|
|
1220
|
+
};
|
|
1051
1221
|
default:
|
|
1052
1222
|
return null;
|
|
1053
1223
|
}
|
|
@@ -1130,14 +1300,14 @@ var init_validate = __esm({
|
|
|
1130
1300
|
// node_modules/@xubylele/schema-forge-core/dist/core/fs.js
|
|
1131
1301
|
async function ensureDir2(dirPath) {
|
|
1132
1302
|
try {
|
|
1133
|
-
await
|
|
1303
|
+
await import_fs3.promises.mkdir(dirPath, { recursive: true });
|
|
1134
1304
|
} catch (error2) {
|
|
1135
1305
|
throw new Error(`Failed to create directory ${dirPath}: ${error2}`);
|
|
1136
1306
|
}
|
|
1137
1307
|
}
|
|
1138
1308
|
async function fileExists2(filePath) {
|
|
1139
1309
|
try {
|
|
1140
|
-
await
|
|
1310
|
+
await import_fs3.promises.access(filePath);
|
|
1141
1311
|
return true;
|
|
1142
1312
|
} catch {
|
|
1143
1313
|
return false;
|
|
@@ -1145,7 +1315,7 @@ async function fileExists2(filePath) {
|
|
|
1145
1315
|
}
|
|
1146
1316
|
async function readTextFile2(filePath) {
|
|
1147
1317
|
try {
|
|
1148
|
-
return await
|
|
1318
|
+
return await import_fs3.promises.readFile(filePath, "utf-8");
|
|
1149
1319
|
} catch (error2) {
|
|
1150
1320
|
throw new Error(`Failed to read file ${filePath}: ${error2}`);
|
|
1151
1321
|
}
|
|
@@ -1154,7 +1324,7 @@ async function writeTextFile2(filePath, content) {
|
|
|
1154
1324
|
try {
|
|
1155
1325
|
const dir = import_path3.default.dirname(filePath);
|
|
1156
1326
|
await ensureDir2(dir);
|
|
1157
|
-
await
|
|
1327
|
+
await import_fs3.promises.writeFile(filePath, content, "utf-8");
|
|
1158
1328
|
} catch (error2) {
|
|
1159
1329
|
throw new Error(`Failed to write file ${filePath}: ${error2}`);
|
|
1160
1330
|
}
|
|
@@ -1182,7 +1352,7 @@ async function writeJsonFile2(filePath, data) {
|
|
|
1182
1352
|
async function findFiles(dirPath, pattern) {
|
|
1183
1353
|
const results = [];
|
|
1184
1354
|
try {
|
|
1185
|
-
const items = await
|
|
1355
|
+
const items = await import_fs3.promises.readdir(dirPath, { withFileTypes: true });
|
|
1186
1356
|
for (const item of items) {
|
|
1187
1357
|
const fullPath = import_path3.default.join(dirPath, item.name);
|
|
1188
1358
|
if (item.isDirectory()) {
|
|
@@ -1197,11 +1367,11 @@ async function findFiles(dirPath, pattern) {
|
|
|
1197
1367
|
}
|
|
1198
1368
|
return results;
|
|
1199
1369
|
}
|
|
1200
|
-
var
|
|
1370
|
+
var import_fs3, import_path3;
|
|
1201
1371
|
var init_fs = __esm({
|
|
1202
1372
|
"node_modules/@xubylele/schema-forge-core/dist/core/fs.js"() {
|
|
1203
1373
|
"use strict";
|
|
1204
|
-
|
|
1374
|
+
import_fs3 = require("fs");
|
|
1205
1375
|
import_path3 = __toESM(require("path"), 1);
|
|
1206
1376
|
}
|
|
1207
1377
|
});
|
|
@@ -1222,9 +1392,21 @@ async function schemaToState(schema) {
|
|
|
1222
1392
|
...column.foreignKey !== void 0 && { foreignKey: column.foreignKey }
|
|
1223
1393
|
};
|
|
1224
1394
|
}
|
|
1395
|
+
let policies;
|
|
1396
|
+
if (table.policies?.length) {
|
|
1397
|
+
policies = {};
|
|
1398
|
+
for (const p of table.policies) {
|
|
1399
|
+
policies[p.name] = {
|
|
1400
|
+
command: p.command,
|
|
1401
|
+
...p.using !== void 0 && { using: p.using },
|
|
1402
|
+
...p.withCheck !== void 0 && { withCheck: p.withCheck }
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1225
1406
|
tables[tableName] = {
|
|
1226
1407
|
columns,
|
|
1227
|
-
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn }
|
|
1408
|
+
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn },
|
|
1409
|
+
...policies !== void 0 && { policies }
|
|
1228
1410
|
};
|
|
1229
1411
|
}
|
|
1230
1412
|
return {
|
|
@@ -1259,9 +1441,20 @@ var init_state_manager = __esm({
|
|
|
1259
1441
|
});
|
|
1260
1442
|
|
|
1261
1443
|
// node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js
|
|
1444
|
+
function generateEnableRls(tableName) {
|
|
1445
|
+
return `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`;
|
|
1446
|
+
}
|
|
1262
1447
|
function generateSql(diff, provider, sqlConfig) {
|
|
1263
1448
|
const statements = [];
|
|
1449
|
+
const enabledRlsTables = /* @__PURE__ */ new Set();
|
|
1264
1450
|
for (const operation of diff.operations) {
|
|
1451
|
+
if (operation.kind === "create_policy" || operation.kind === "modify_policy") {
|
|
1452
|
+
const tableName = operation.tableName;
|
|
1453
|
+
if (!enabledRlsTables.has(tableName)) {
|
|
1454
|
+
statements.push(generateEnableRls(tableName));
|
|
1455
|
+
enabledRlsTables.add(tableName);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1265
1458
|
const sql = generateOperation(operation, provider, sqlConfig);
|
|
1266
1459
|
if (sql) {
|
|
1267
1460
|
statements.push(sql);
|
|
@@ -1291,6 +1484,12 @@ function generateOperation(operation, provider, sqlConfig) {
|
|
|
1291
1484
|
return generateDropPrimaryKeyConstraint(operation.tableName);
|
|
1292
1485
|
case "add_primary_key_constraint":
|
|
1293
1486
|
return generateAddPrimaryKeyConstraint(operation.tableName, operation.columnName);
|
|
1487
|
+
case "create_policy":
|
|
1488
|
+
return generateCreatePolicy(operation.tableName, operation.policy);
|
|
1489
|
+
case "drop_policy":
|
|
1490
|
+
return generateDropPolicy(operation.tableName, operation.policyName);
|
|
1491
|
+
case "modify_policy":
|
|
1492
|
+
return generateModifyPolicy(operation.tableName, operation.policyName, operation.policy);
|
|
1294
1493
|
}
|
|
1295
1494
|
}
|
|
1296
1495
|
function generateCreateTable(table, provider, sqlConfig) {
|
|
@@ -1371,6 +1570,25 @@ function generateAlterColumnNullability(tableName, columnName, toNullable) {
|
|
|
1371
1570
|
}
|
|
1372
1571
|
return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET NOT NULL;`;
|
|
1373
1572
|
}
|
|
1573
|
+
function generateCreatePolicy(tableName, policy) {
|
|
1574
|
+
const command = policy.command.toUpperCase();
|
|
1575
|
+
const parts = [`CREATE POLICY "${policy.name}" ON ${tableName} FOR ${command}`];
|
|
1576
|
+
if (policy.using !== void 0 && policy.using !== "") {
|
|
1577
|
+
parts.push(`USING (${policy.using})`);
|
|
1578
|
+
}
|
|
1579
|
+
if (policy.withCheck !== void 0 && policy.withCheck !== "") {
|
|
1580
|
+
parts.push(`WITH CHECK (${policy.withCheck})`);
|
|
1581
|
+
}
|
|
1582
|
+
return parts.join(" ") + ";";
|
|
1583
|
+
}
|
|
1584
|
+
function generateDropPolicy(tableName, policyName) {
|
|
1585
|
+
return `DROP POLICY "${policyName}" ON ${tableName};`;
|
|
1586
|
+
}
|
|
1587
|
+
function generateModifyPolicy(tableName, policyName, policy) {
|
|
1588
|
+
const drop = generateDropPolicy(tableName, policyName);
|
|
1589
|
+
const create = generateCreatePolicy(tableName, policy);
|
|
1590
|
+
return drop + "\n\n" + create;
|
|
1591
|
+
}
|
|
1374
1592
|
var init_sql_generator = __esm({
|
|
1375
1593
|
"node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js"() {
|
|
1376
1594
|
"use strict";
|
|
@@ -2204,7 +2422,7 @@ var init_schema_to_dsl = __esm({
|
|
|
2204
2422
|
|
|
2205
2423
|
// node_modules/@xubylele/schema-forge-core/dist/core/sql/load-migrations.js
|
|
2206
2424
|
async function loadMigrationSqlInput(inputPath) {
|
|
2207
|
-
const stats = await
|
|
2425
|
+
const stats = await import_fs5.promises.stat(inputPath);
|
|
2208
2426
|
if (stats.isFile()) {
|
|
2209
2427
|
if (!inputPath.toLowerCase().endsWith(".sql")) {
|
|
2210
2428
|
throw new Error(`Input file must be a .sql file: ${inputPath}`);
|
|
@@ -2225,11 +2443,11 @@ async function loadMigrationSqlInput(inputPath) {
|
|
|
2225
2443
|
}
|
|
2226
2444
|
return result;
|
|
2227
2445
|
}
|
|
2228
|
-
var
|
|
2446
|
+
var import_fs5, import_path5;
|
|
2229
2447
|
var init_load_migrations = __esm({
|
|
2230
2448
|
"node_modules/@xubylele/schema-forge-core/dist/core/sql/load-migrations.js"() {
|
|
2231
2449
|
"use strict";
|
|
2232
|
-
|
|
2450
|
+
import_fs5 = require("fs");
|
|
2233
2451
|
import_path5 = __toESM(require("path"), 1);
|
|
2234
2452
|
init_fs();
|
|
2235
2453
|
}
|
|
@@ -2733,7 +2951,7 @@ var import_commander8 = require("commander");
|
|
|
2733
2951
|
// package.json
|
|
2734
2952
|
var package_default = {
|
|
2735
2953
|
name: "@xubylele/schema-forge",
|
|
2736
|
-
version: "1.
|
|
2954
|
+
version: "1.12.0",
|
|
2737
2955
|
description: "Universal migration generator from schema DSL",
|
|
2738
2956
|
main: "dist/cli.js",
|
|
2739
2957
|
type: "commonjs",
|
|
@@ -2787,13 +3005,14 @@ var package_default = {
|
|
|
2787
3005
|
boxen: "^8.0.1",
|
|
2788
3006
|
chalk: "^5.6.2",
|
|
2789
3007
|
commander: "^14.0.3",
|
|
2790
|
-
pg: "^8.19.0"
|
|
3008
|
+
pg: "^8.19.0",
|
|
3009
|
+
"update-notifier": "^7.3.1"
|
|
2791
3010
|
},
|
|
2792
3011
|
devDependencies: {
|
|
2793
3012
|
"@changesets/cli": "^2.30.0",
|
|
2794
3013
|
"@types/node": "^25.2.3",
|
|
2795
3014
|
"@types/pg": "^8.18.0",
|
|
2796
|
-
"@xubylele/schema-forge-core": "^1.
|
|
3015
|
+
"@xubylele/schema-forge-core": "^1.4.0",
|
|
2797
3016
|
testcontainers: "^11.8.1",
|
|
2798
3017
|
"ts-node": "^10.9.2",
|
|
2799
3018
|
tsup: "^8.5.1",
|
|
@@ -2802,10 +3021,6 @@ var package_default = {
|
|
|
2802
3021
|
}
|
|
2803
3022
|
};
|
|
2804
3023
|
|
|
2805
|
-
// src/commands/diff.ts
|
|
2806
|
-
var import_commander = require("commander");
|
|
2807
|
-
var import_path7 = __toESM(require("path"));
|
|
2808
|
-
|
|
2809
3024
|
// src/core/fs.ts
|
|
2810
3025
|
var import_fs = require("fs");
|
|
2811
3026
|
var import_path = __toESM(require("path"));
|
|
@@ -2884,6 +3099,91 @@ function getStatePath(root, config) {
|
|
|
2884
3099
|
return import_path2.default.join(schemaForgeDir, fileName);
|
|
2885
3100
|
}
|
|
2886
3101
|
|
|
3102
|
+
// src/utils/exitCodes.ts
|
|
3103
|
+
var EXIT_CODES = {
|
|
3104
|
+
SUCCESS: 0,
|
|
3105
|
+
VALIDATION_ERROR: 1,
|
|
3106
|
+
DRIFT_DETECTED: 2,
|
|
3107
|
+
CI_DESTRUCTIVE: 3
|
|
3108
|
+
};
|
|
3109
|
+
function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
|
|
3110
|
+
return isCIEnvironment && hasDestructiveFindings2 && !isForceEnabled;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/utils/output.ts
|
|
3114
|
+
var import_boxen = __toESM(require("boxen"));
|
|
3115
|
+
var import_chalk = require("chalk");
|
|
3116
|
+
var isInteractive = Boolean(process.stdout?.isTTY);
|
|
3117
|
+
var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
|
|
3118
|
+
var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
|
|
3119
|
+
var theme = {
|
|
3120
|
+
primary: color.cyanBright,
|
|
3121
|
+
success: color.hex("#00FF88"),
|
|
3122
|
+
warning: color.hex("#FFD166"),
|
|
3123
|
+
error: color.hex("#EF476F"),
|
|
3124
|
+
accent: color.magentaBright
|
|
3125
|
+
};
|
|
3126
|
+
function success(message) {
|
|
3127
|
+
const text = theme.success(`[OK] ${message}`);
|
|
3128
|
+
if (!isInteractive) {
|
|
3129
|
+
console.log(text);
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
try {
|
|
3133
|
+
console.log(
|
|
3134
|
+
(0, import_boxen.default)(text, {
|
|
3135
|
+
padding: 1,
|
|
3136
|
+
borderColor: "cyan",
|
|
3137
|
+
borderStyle: "round"
|
|
3138
|
+
})
|
|
3139
|
+
);
|
|
3140
|
+
} catch {
|
|
3141
|
+
console.log(text);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
function info(message) {
|
|
3145
|
+
console.log(theme.primary(message));
|
|
3146
|
+
}
|
|
3147
|
+
function warning(message) {
|
|
3148
|
+
console.warn(theme.warning(`[WARN] ${message}`));
|
|
3149
|
+
}
|
|
3150
|
+
function error(message) {
|
|
3151
|
+
console.error(theme.error(`[ERROR] ${message}`));
|
|
3152
|
+
}
|
|
3153
|
+
function forceWarning(message) {
|
|
3154
|
+
console.error(theme.warning(`[FORCE] ${message}`));
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// src/commands/config.ts
|
|
3158
|
+
var MIGRATION_FORMATS = ["hyphen", "underscore"];
|
|
3159
|
+
async function runConfig(options) {
|
|
3160
|
+
const root = getProjectRoot();
|
|
3161
|
+
const configPath = getConfigPath(root);
|
|
3162
|
+
if (!await fileExists(configPath)) {
|
|
3163
|
+
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
3164
|
+
}
|
|
3165
|
+
const config = await readJsonFile(configPath, {});
|
|
3166
|
+
if (options.migrationFormat !== void 0) {
|
|
3167
|
+
const format = options.migrationFormat.trim().toLowerCase();
|
|
3168
|
+
if (!MIGRATION_FORMATS.includes(format)) {
|
|
3169
|
+
throw new Error(
|
|
3170
|
+
`Invalid migration format "${options.migrationFormat}". Allowed: ${MIGRATION_FORMATS.join(", ")}.`
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
config.migrationFileNameFormat = format;
|
|
3174
|
+
}
|
|
3175
|
+
await writeJsonFile(configPath, config);
|
|
3176
|
+
success(`Updated ${configPath}`);
|
|
3177
|
+
if (options.migrationFormat !== void 0) {
|
|
3178
|
+
success(`migrationFileNameFormat set to "${config.migrationFileNameFormat}" (${config.migrationFileNameFormat === "hyphen" ? "timestamp-name.sql" : "timestamp_name.sql"})`);
|
|
3179
|
+
}
|
|
3180
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/commands/diff.ts
|
|
3184
|
+
var import_commander = require("commander");
|
|
3185
|
+
var import_path7 = __toESM(require("path"));
|
|
3186
|
+
|
|
2887
3187
|
// src/core/provider.ts
|
|
2888
3188
|
var DEFAULT_PROVIDER = "postgres";
|
|
2889
3189
|
function resolveProvider(provider) {
|
|
@@ -3004,65 +3304,6 @@ async function withPostgresQueryExecutor(connectionString, run) {
|
|
|
3004
3304
|
}
|
|
3005
3305
|
}
|
|
3006
3306
|
|
|
3007
|
-
// src/utils/exitCodes.ts
|
|
3008
|
-
var EXIT_CODES = {
|
|
3009
|
-
/** Successful operation */
|
|
3010
|
-
SUCCESS: 0,
|
|
3011
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
3012
|
-
VALIDATION_ERROR: 1,
|
|
3013
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
3014
|
-
DRIFT_DETECTED: 2,
|
|
3015
|
-
/** Destructive operation detected in CI environment without --force */
|
|
3016
|
-
CI_DESTRUCTIVE: 3
|
|
3017
|
-
};
|
|
3018
|
-
function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
|
|
3019
|
-
return isCIEnvironment && hasDestructiveFindings2 && !isForceEnabled;
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
// src/utils/output.ts
|
|
3023
|
-
var import_boxen = __toESM(require("boxen"));
|
|
3024
|
-
var import_chalk = require("chalk");
|
|
3025
|
-
var isInteractive = Boolean(process.stdout?.isTTY);
|
|
3026
|
-
var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
|
|
3027
|
-
var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
|
|
3028
|
-
var theme = {
|
|
3029
|
-
primary: color.cyanBright,
|
|
3030
|
-
success: color.hex("#00FF88"),
|
|
3031
|
-
warning: color.hex("#FFD166"),
|
|
3032
|
-
error: color.hex("#EF476F"),
|
|
3033
|
-
accent: color.magentaBright
|
|
3034
|
-
};
|
|
3035
|
-
function success(message) {
|
|
3036
|
-
const text = theme.success(`[OK] ${message}`);
|
|
3037
|
-
if (!isInteractive) {
|
|
3038
|
-
console.log(text);
|
|
3039
|
-
return;
|
|
3040
|
-
}
|
|
3041
|
-
try {
|
|
3042
|
-
console.log(
|
|
3043
|
-
(0, import_boxen.default)(text, {
|
|
3044
|
-
padding: 1,
|
|
3045
|
-
borderColor: "cyan",
|
|
3046
|
-
borderStyle: "round"
|
|
3047
|
-
})
|
|
3048
|
-
);
|
|
3049
|
-
} catch {
|
|
3050
|
-
console.log(text);
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
function info(message) {
|
|
3054
|
-
console.log(theme.primary(message));
|
|
3055
|
-
}
|
|
3056
|
-
function warning(message) {
|
|
3057
|
-
console.warn(theme.warning(`[WARN] ${message}`));
|
|
3058
|
-
}
|
|
3059
|
-
function error(message) {
|
|
3060
|
-
console.error(theme.error(`[ERROR] ${message}`));
|
|
3061
|
-
}
|
|
3062
|
-
function forceWarning(message) {
|
|
3063
|
-
console.error(theme.warning(`[FORCE] ${message}`));
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
3307
|
// src/utils/prompt.ts
|
|
3067
3308
|
var import_node_readline = __toESM(require("readline"));
|
|
3068
3309
|
function isCI() {
|
|
@@ -3306,6 +3547,10 @@ function nowTimestamp2() {
|
|
|
3306
3547
|
function slugifyName2(name) {
|
|
3307
3548
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "migration";
|
|
3308
3549
|
}
|
|
3550
|
+
function migrationFileName(timestamp, slug, format = "hyphen") {
|
|
3551
|
+
const sep = format === "underscore" ? "_" : "-";
|
|
3552
|
+
return `${timestamp}${sep}${slug}.sql`;
|
|
3553
|
+
}
|
|
3309
3554
|
|
|
3310
3555
|
// src/commands/generate.ts
|
|
3311
3556
|
var REQUIRED_CONFIG_FIELDS = [
|
|
@@ -3313,6 +3558,19 @@ var REQUIRED_CONFIG_FIELDS = [
|
|
|
3313
3558
|
"stateFile",
|
|
3314
3559
|
"outputDir"
|
|
3315
3560
|
];
|
|
3561
|
+
var MIGRATION_FORMATS2 = ["hyphen", "underscore"];
|
|
3562
|
+
function resolveMigrationFormat(cliFormat, configFormat) {
|
|
3563
|
+
if (cliFormat !== void 0 && cliFormat !== "") {
|
|
3564
|
+
const normalized = cliFormat.trim().toLowerCase();
|
|
3565
|
+
if (MIGRATION_FORMATS2.includes(normalized)) {
|
|
3566
|
+
return normalized;
|
|
3567
|
+
}
|
|
3568
|
+
throw new Error(
|
|
3569
|
+
`Invalid --migration-format "${cliFormat}". Allowed: ${MIGRATION_FORMATS2.join(", ")}.`
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
return configFormat ?? "hyphen";
|
|
3573
|
+
}
|
|
3316
3574
|
function resolveConfigPath3(root, targetPath) {
|
|
3317
3575
|
return import_path9.default.isAbsolute(targetPath) ? targetPath : import_path9.default.join(root, targetPath);
|
|
3318
3576
|
}
|
|
@@ -3393,7 +3651,11 @@ Remove --safe flag or modify schema to avoid destructive changes.`
|
|
|
3393
3651
|
const sql = await generateSql2(diff, provider, config.sql);
|
|
3394
3652
|
const timestamp = nowTimestamp2();
|
|
3395
3653
|
const slug = slugifyName2(options.name ?? "migration");
|
|
3396
|
-
const
|
|
3654
|
+
const format = resolveMigrationFormat(
|
|
3655
|
+
options.migrationFormat,
|
|
3656
|
+
config.migrationFileNameFormat
|
|
3657
|
+
);
|
|
3658
|
+
const fileName = migrationFileName(timestamp, slug, format);
|
|
3397
3659
|
await ensureDir(outputDir);
|
|
3398
3660
|
const migrationPath = import_path9.default.join(outputDir, fileName);
|
|
3399
3661
|
await writeTextFile(migrationPath, sql + "\n");
|
|
@@ -3529,6 +3791,9 @@ table users {
|
|
|
3529
3791
|
timestampDefault: "now()"
|
|
3530
3792
|
}
|
|
3531
3793
|
};
|
|
3794
|
+
if (provider === "supabase") {
|
|
3795
|
+
config.migrationFileNameFormat = "hyphen";
|
|
3796
|
+
}
|
|
3532
3797
|
await writeJsonFile(configPath, config);
|
|
3533
3798
|
success(`Created ${configPath}`);
|
|
3534
3799
|
const state = {
|
|
@@ -3689,6 +3954,35 @@ function getCliMetaPath() {
|
|
|
3689
3954
|
function getReleaseUrl(version) {
|
|
3690
3955
|
return `https://github.com/xubylele/schema-forge/releases/tag/v${version}`;
|
|
3691
3956
|
}
|
|
3957
|
+
function extractChangelogSection(changelogText, version) {
|
|
3958
|
+
const heading = `## ${version}`;
|
|
3959
|
+
const idx = changelogText.indexOf(heading);
|
|
3960
|
+
if (idx === -1) {
|
|
3961
|
+
return null;
|
|
3962
|
+
}
|
|
3963
|
+
const start = idx + heading.length;
|
|
3964
|
+
const rest = changelogText.slice(start);
|
|
3965
|
+
const nextHeading = rest.indexOf("\n## ");
|
|
3966
|
+
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
|
3967
|
+
return section.trim() || null;
|
|
3968
|
+
}
|
|
3969
|
+
async function fetchChangelogForVersion(version) {
|
|
3970
|
+
const urls = [
|
|
3971
|
+
`https://raw.githubusercontent.com/xubylele/schema-forge/v${version}/CHANGELOG.md`,
|
|
3972
|
+
"https://raw.githubusercontent.com/xubylele/schema-forge/main/CHANGELOG.md"
|
|
3973
|
+
];
|
|
3974
|
+
for (const url of urls) {
|
|
3975
|
+
try {
|
|
3976
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
3977
|
+
if (!res.ok) continue;
|
|
3978
|
+
const text = await res.text();
|
|
3979
|
+
const section = extractChangelogSection(text, version);
|
|
3980
|
+
if (section) return section;
|
|
3981
|
+
} catch {
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
return null;
|
|
3985
|
+
}
|
|
3692
3986
|
function shouldShowWhatsNew(argv) {
|
|
3693
3987
|
if (argv.length === 0) {
|
|
3694
3988
|
return false;
|
|
@@ -3708,7 +4002,14 @@ async function showWhatsNewIfUpdated(currentVersion, argv) {
|
|
|
3708
4002
|
if (meta.lastSeenVersion === currentVersion) {
|
|
3709
4003
|
return;
|
|
3710
4004
|
}
|
|
3711
|
-
|
|
4005
|
+
const section = await fetchChangelogForVersion(currentVersion);
|
|
4006
|
+
if (section) {
|
|
4007
|
+
info(`What's new in schema-forge v${currentVersion}:`);
|
|
4008
|
+
info(section);
|
|
4009
|
+
info(`Release: ${getReleaseUrl(currentVersion)}`);
|
|
4010
|
+
} else {
|
|
4011
|
+
info(`What's new in schema-forge v${currentVersion}: ${getReleaseUrl(currentVersion)}`);
|
|
4012
|
+
}
|
|
3712
4013
|
await writeJsonFile(metaPath, { lastSeenVersion: currentVersion });
|
|
3713
4014
|
} catch {
|
|
3714
4015
|
}
|
|
@@ -3752,7 +4053,7 @@ program.command("init").description(
|
|
|
3752
4053
|
await handleError(error2);
|
|
3753
4054
|
}
|
|
3754
4055
|
});
|
|
3755
|
-
program.command("generate").description("Generate SQL from schema files. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--name <string>", "Schema name to generate").action(async (options) => {
|
|
4056
|
+
program.command("generate").description("Generate SQL from schema files. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--name <string>", "Schema name to generate").option("--migration-format <format>", "Migration file name: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql, Supabase CLI style)").action(async (options) => {
|
|
3756
4057
|
try {
|
|
3757
4058
|
const globalOptions = program.opts();
|
|
3758
4059
|
validateFlagExclusivity(globalOptions);
|
|
@@ -3791,6 +4092,13 @@ program.command("import").description("Import schema from SQL migrations").argum
|
|
|
3791
4092
|
await handleError(error2);
|
|
3792
4093
|
}
|
|
3793
4094
|
});
|
|
4095
|
+
program.command("config").description("Update schemaforge/config.json settings").command("migration-format <format>").description("Set migration file name format: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql)").action(async (format) => {
|
|
4096
|
+
try {
|
|
4097
|
+
await runConfig({ migrationFormat: format });
|
|
4098
|
+
} catch (error2) {
|
|
4099
|
+
await handleError(error2);
|
|
4100
|
+
}
|
|
4101
|
+
});
|
|
3794
4102
|
program.command("validate").description("Detect destructive or risky schema changes. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--json", "Output structured JSON").option("--url <string>", "PostgreSQL connection URL for live drift validation (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
3795
4103
|
try {
|
|
3796
4104
|
const globalOptions = program.opts();
|
|
@@ -3800,10 +4108,21 @@ program.command("validate").description("Detect destructive or risky schema chan
|
|
|
3800
4108
|
await handleError(error2);
|
|
3801
4109
|
}
|
|
3802
4110
|
});
|
|
4111
|
+
function shouldCheckForUpdate(argv) {
|
|
4112
|
+
if (process.env.CI === "true") {
|
|
4113
|
+
return false;
|
|
4114
|
+
}
|
|
4115
|
+
const onlyHelpOrVersion = argv.length === 0 || argv.length === 1 && ["--help", "-h", "--version", "-V"].includes(argv[0]);
|
|
4116
|
+
return !onlyHelpOrVersion;
|
|
4117
|
+
}
|
|
3803
4118
|
async function main() {
|
|
3804
4119
|
const argv = process.argv.slice(2);
|
|
3805
4120
|
await seedLastSeenVersion(package_default.version);
|
|
3806
4121
|
await showWhatsNewIfUpdated(package_default.version, argv);
|
|
4122
|
+
if (shouldCheckForUpdate(argv)) {
|
|
4123
|
+
import("update-notifier").then((m) => m.default({ pkg: package_default, shouldNotifyInNpmScript: false }).notify()).catch(() => {
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
3807
4126
|
program.parse(process.argv);
|
|
3808
4127
|
if (!argv.length) {
|
|
3809
4128
|
program.outputHelp();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xubylele/schema-forge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Universal migration generator from schema DSL",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -54,13 +54,14 @@
|
|
|
54
54
|
"boxen": "^8.0.1",
|
|
55
55
|
"chalk": "^5.6.2",
|
|
56
56
|
"commander": "^14.0.3",
|
|
57
|
-
"pg": "^8.19.0"
|
|
57
|
+
"pg": "^8.19.0",
|
|
58
|
+
"update-notifier": "^7.3.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@changesets/cli": "^2.30.0",
|
|
61
62
|
"@types/node": "^25.2.3",
|
|
62
63
|
"@types/pg": "^8.18.0",
|
|
63
|
-
"@xubylele/schema-forge-core": "^1.
|
|
64
|
+
"@xubylele/schema-forge-core": "^1.4.0",
|
|
64
65
|
"testcontainers": "^11.8.1",
|
|
65
66
|
"ts-node": "^10.9.2",
|
|
66
67
|
"tsup": "^8.5.1",
|