@xubylele/schema-forge 1.11.0 → 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 +22 -0
- package/dist/api.d.ts +0 -49
- package/dist/api.js +222 -8
- package/dist/cli.js +274 -12
- 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
|
|
|
@@ -324,6 +325,7 @@ Live `--json` output returns a structured `DriftReport`:
|
|
|
324
325
|
|
|
325
326
|
Validation checks include:
|
|
326
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).
|
|
327
329
|
- Dropped tables (`DROP_TABLE`, error)
|
|
328
330
|
- Dropped columns (`DROP_COLUMN`, error)
|
|
329
331
|
- Column type changes (`ALTER_COLUMN_TYPE`, warning/error based on compatibility heuristics)
|
|
@@ -566,6 +568,26 @@ table table_name {
|
|
|
566
568
|
- `default <value>` - Default value (e.g., `default now()`, `default false`, `default 0`)
|
|
567
569
|
- `fk <table>.<column>` - Foreign key reference (e.g., `fk users.id`)
|
|
568
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
|
+
|
|
569
591
|
### Examples
|
|
570
592
|
|
|
571
593
|
#### Simple table
|
package/dist/api.d.ts
CHANGED
|
@@ -11,14 +11,12 @@ interface DoctorOptions {
|
|
|
11
11
|
schema?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/** Migration file name format: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql, Supabase CLI style). */
|
|
15
14
|
type MigrationFileNameFormat = 'hyphen' | 'underscore';
|
|
16
15
|
|
|
17
16
|
interface GenerateOptions {
|
|
18
17
|
name?: string;
|
|
19
18
|
safe?: boolean;
|
|
20
19
|
force?: boolean;
|
|
21
|
-
/** Override migration file name format: hyphen (timestamp-name.sql) or underscore (timestamp_name.sql). */
|
|
22
20
|
migrationFormat?: MigrationFileNameFormat;
|
|
23
21
|
}
|
|
24
22
|
|
|
@@ -44,69 +42,22 @@ interface ValidateOptions {
|
|
|
44
42
|
force?: boolean;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
/**
|
|
48
|
-
* Exit codes used throughout the CLI for deterministic behavior
|
|
49
|
-
*
|
|
50
|
-
* @see SF-106 Standardize Exit Codes
|
|
51
|
-
*/
|
|
52
45
|
declare const EXIT_CODES: {
|
|
53
|
-
/** Successful operation */
|
|
54
46
|
readonly SUCCESS: 0;
|
|
55
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
56
47
|
readonly VALIDATION_ERROR: 1;
|
|
57
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
58
48
|
readonly DRIFT_DETECTED: 2;
|
|
59
|
-
/** Destructive operation detected in CI environment without --force */
|
|
60
49
|
readonly CI_DESTRUCTIVE: 3;
|
|
61
50
|
};
|
|
62
51
|
|
|
63
|
-
/**
|
|
64
|
-
* Programmatic API for Schema Forge.
|
|
65
|
-
* Use this entrypoint when integrating from Node (e.g. scripts, GitHub Actions)
|
|
66
|
-
* instead of invoking the CLI via shell.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* const { generate, EXIT_CODES } = require('@xubylele/schema-forge/api');
|
|
70
|
-
* const result = await generate({ name: 'MyMigration' });
|
|
71
|
-
* if (result.exitCode !== EXIT_CODES.SUCCESS) process.exit(result.exitCode);
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Result of a programmatic command run. Exit codes match the CLI contract.
|
|
76
|
-
* @see docs/exit-codes.json
|
|
77
|
-
*/
|
|
78
52
|
interface RunResult {
|
|
79
53
|
exitCode: number;
|
|
80
54
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Initialize a new schema project in the current directory.
|
|
83
|
-
* @param options.provider - Database provider: 'postgres' (default) or 'supabase'. Supabase uses supabase/migrations for output.
|
|
84
|
-
*/
|
|
85
55
|
declare function init(options?: InitOptions): Promise<RunResult>;
|
|
86
|
-
/**
|
|
87
|
-
* Generate SQL migration from schema files.
|
|
88
|
-
*/
|
|
89
56
|
declare function generate(options?: GenerateOptions): Promise<RunResult>;
|
|
90
|
-
/**
|
|
91
|
-
* Compare two schema versions and generate migration SQL (optionally against live DB).
|
|
92
|
-
*/
|
|
93
57
|
declare function diff(options?: DiffOptions): Promise<RunResult>;
|
|
94
|
-
/**
|
|
95
|
-
* Check live database drift against state.
|
|
96
|
-
*/
|
|
97
58
|
declare function doctor(options?: DoctorOptions): Promise<RunResult>;
|
|
98
|
-
/**
|
|
99
|
-
* Validate schema and optionally check for destructive changes or live drift.
|
|
100
|
-
*/
|
|
101
59
|
declare function validate(options?: ValidateOptions): Promise<RunResult>;
|
|
102
|
-
/**
|
|
103
|
-
* Extract normalized live schema from PostgreSQL.
|
|
104
|
-
*/
|
|
105
60
|
declare function introspect(options?: IntrospectOptions): Promise<RunResult>;
|
|
106
|
-
/**
|
|
107
|
-
* Import schema from SQL migrations.
|
|
108
|
-
* @param inputPath - Path to .sql file or migrations directory
|
|
109
|
-
*/
|
|
110
61
|
declare function importSchema(inputPath: string, options?: ImportOptions): Promise<RunResult>;
|
|
111
62
|
|
|
112
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) {
|
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
|
}
|
|
@@ -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";
|
|
@@ -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",
|
|
@@ -2882,13 +3101,9 @@ function getStatePath(root, config) {
|
|
|
2882
3101
|
|
|
2883
3102
|
// src/utils/exitCodes.ts
|
|
2884
3103
|
var EXIT_CODES = {
|
|
2885
|
-
/** Successful operation */
|
|
2886
3104
|
SUCCESS: 0,
|
|
2887
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
2888
3105
|
VALIDATION_ERROR: 1,
|
|
2889
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
2890
3106
|
DRIFT_DETECTED: 2,
|
|
2891
|
-
/** Destructive operation detected in CI environment without --force */
|
|
2892
3107
|
CI_DESTRUCTIVE: 3
|
|
2893
3108
|
};
|
|
2894
3109
|
function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
|
|
@@ -3739,6 +3954,35 @@ function getCliMetaPath() {
|
|
|
3739
3954
|
function getReleaseUrl(version) {
|
|
3740
3955
|
return `https://github.com/xubylele/schema-forge/releases/tag/v${version}`;
|
|
3741
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
|
+
}
|
|
3742
3986
|
function shouldShowWhatsNew(argv) {
|
|
3743
3987
|
if (argv.length === 0) {
|
|
3744
3988
|
return false;
|
|
@@ -3758,7 +4002,14 @@ async function showWhatsNewIfUpdated(currentVersion, argv) {
|
|
|
3758
4002
|
if (meta.lastSeenVersion === currentVersion) {
|
|
3759
4003
|
return;
|
|
3760
4004
|
}
|
|
3761
|
-
|
|
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
|
+
}
|
|
3762
4013
|
await writeJsonFile(metaPath, { lastSeenVersion: currentVersion });
|
|
3763
4014
|
} catch {
|
|
3764
4015
|
}
|
|
@@ -3857,10 +4108,21 @@ program.command("validate").description("Detect destructive or risky schema chan
|
|
|
3857
4108
|
await handleError(error2);
|
|
3858
4109
|
}
|
|
3859
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
|
+
}
|
|
3860
4118
|
async function main() {
|
|
3861
4119
|
const argv = process.argv.slice(2);
|
|
3862
4120
|
await seedLastSeenVersion(package_default.version);
|
|
3863
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
|
+
}
|
|
3864
4126
|
program.parse(process.argv);
|
|
3865
4127
|
if (!argv.length) {
|
|
3866
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",
|