@xubylele/schema-forge 1.11.0 → 1.12.1
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 +35 -0
- package/dist/api.d.ts +0 -49
- package/dist/api.js +400 -15
- package/dist/cli.js +452 -19
- package/package.json +4 -3
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,85 @@ 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 toRoles;
|
|
275
|
+
let lineIdx = startLine + 1;
|
|
276
|
+
while (lineIdx < lines.length) {
|
|
277
|
+
const cleaned = cleanLine(lines[lineIdx]);
|
|
278
|
+
if (!cleaned) {
|
|
279
|
+
lineIdx++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (cleaned.startsWith("for ")) {
|
|
283
|
+
const rest = cleaned.slice(4).trim().toLowerCase();
|
|
284
|
+
const parts = rest.split(/\s+/);
|
|
285
|
+
const cmd = parts[0];
|
|
286
|
+
if (!POLICY_COMMANDS.includes(cmd)) {
|
|
287
|
+
throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, delete, or all`);
|
|
288
|
+
}
|
|
289
|
+
if (command !== void 0) {
|
|
290
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'for' in policy`);
|
|
291
|
+
}
|
|
292
|
+
command = cmd;
|
|
293
|
+
if (parts.length > 1 && parts[1] === "to" && parts.length > 2) {
|
|
294
|
+
toRoles = parts.slice(2);
|
|
295
|
+
}
|
|
296
|
+
lineIdx++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (cleaned.startsWith("to ")) {
|
|
300
|
+
if (toRoles !== void 0) {
|
|
301
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'to' in policy`);
|
|
302
|
+
}
|
|
303
|
+
const roles = cleaned.slice(3).trim().split(/\s+/).filter(Boolean);
|
|
304
|
+
if (roles.length > 0) {
|
|
305
|
+
toRoles = roles;
|
|
306
|
+
}
|
|
307
|
+
lineIdx++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (cleaned.startsWith("using ")) {
|
|
311
|
+
if (using !== void 0) {
|
|
312
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'using' in policy`);
|
|
313
|
+
}
|
|
314
|
+
using = cleaned.slice(6).trim();
|
|
315
|
+
lineIdx++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (cleaned.startsWith("with check ")) {
|
|
319
|
+
if (withCheck !== void 0) {
|
|
320
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'with check' in policy`);
|
|
321
|
+
}
|
|
322
|
+
withCheck = cleaned.slice(11).trim();
|
|
323
|
+
lineIdx++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
if (command === void 0) {
|
|
329
|
+
throw new Error(`Line ${startLine + 1}: Policy is missing 'for <command>'`);
|
|
330
|
+
}
|
|
331
|
+
const policy = {
|
|
332
|
+
name: policyName,
|
|
333
|
+
table: tableIdent,
|
|
334
|
+
command,
|
|
335
|
+
...using !== void 0 && { using },
|
|
336
|
+
...withCheck !== void 0 && { withCheck },
|
|
337
|
+
...toRoles !== void 0 && toRoles.length > 0 && { to: toRoles }
|
|
338
|
+
};
|
|
339
|
+
return { policy, nextLineIndex: lineIdx };
|
|
340
|
+
}
|
|
261
341
|
function parseTableBlock(startLine) {
|
|
262
342
|
const firstLine = cleanLine(lines[startLine]);
|
|
263
343
|
const match = firstLine.match(/^table\s+(\w+)\s*\{\s*$/);
|
|
@@ -301,6 +381,7 @@ function parseSchema(source) {
|
|
|
301
381
|
};
|
|
302
382
|
return lineIdx;
|
|
303
383
|
}
|
|
384
|
+
const policyDeclRegex = /^policy\s+"([^"]*)"\s+on\s+\w+/;
|
|
304
385
|
while (currentLine < lines.length) {
|
|
305
386
|
const cleaned = cleanLine(lines[currentLine]);
|
|
306
387
|
if (!cleaned) {
|
|
@@ -309,16 +390,31 @@ function parseSchema(source) {
|
|
|
309
390
|
}
|
|
310
391
|
if (cleaned.startsWith("table ")) {
|
|
311
392
|
currentLine = parseTableBlock(currentLine);
|
|
393
|
+
currentLine++;
|
|
394
|
+
} else if (policyDeclRegex.test(cleaned)) {
|
|
395
|
+
const { policy, nextLineIndex } = parsePolicyBlock(currentLine);
|
|
396
|
+
policyList.push({ policy, startLine: currentLine + 1 });
|
|
397
|
+
currentLine = nextLineIndex;
|
|
312
398
|
} else {
|
|
313
|
-
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table definition.`);
|
|
399
|
+
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table or policy definition.`);
|
|
314
400
|
}
|
|
315
|
-
|
|
401
|
+
}
|
|
402
|
+
for (const { policy, startLine } of policyList) {
|
|
403
|
+
if (!tables[policy.table]) {
|
|
404
|
+
throw new Error(`Line ${startLine}: Policy "${policy.name}" references undefined table "${policy.table}"`);
|
|
405
|
+
}
|
|
406
|
+
if (!tables[policy.table].policies) {
|
|
407
|
+
tables[policy.table].policies = [];
|
|
408
|
+
}
|
|
409
|
+
tables[policy.table].policies.push(policy);
|
|
316
410
|
}
|
|
317
411
|
return { tables };
|
|
318
412
|
}
|
|
413
|
+
var POLICY_COMMANDS;
|
|
319
414
|
var init_parser = __esm({
|
|
320
415
|
"node_modules/@xubylele/schema-forge-core/dist/core/parser.js"() {
|
|
321
416
|
"use strict";
|
|
417
|
+
POLICY_COMMANDS = ["select", "insert", "update", "delete", "all"];
|
|
322
418
|
}
|
|
323
419
|
});
|
|
324
420
|
|
|
@@ -498,6 +594,22 @@ function resolveSchemaPrimaryKey(table) {
|
|
|
498
594
|
function normalizeNullable(nullable) {
|
|
499
595
|
return nullable ?? true;
|
|
500
596
|
}
|
|
597
|
+
function normalizePolicyExpression(s) {
|
|
598
|
+
return (s ?? "").trim().replace(/\s+/g, " ");
|
|
599
|
+
}
|
|
600
|
+
function policyEquals(oldP, newP) {
|
|
601
|
+
if (oldP.command !== newP.command)
|
|
602
|
+
return false;
|
|
603
|
+
if (normalizePolicyExpression(oldP.using) !== normalizePolicyExpression(newP.using))
|
|
604
|
+
return false;
|
|
605
|
+
if (normalizePolicyExpression(oldP.withCheck) !== normalizePolicyExpression(newP.withCheck))
|
|
606
|
+
return false;
|
|
607
|
+
const oldTo = (oldP.to ?? []).slice().sort().join(",");
|
|
608
|
+
const newTo = (newP.to ?? []).slice().sort().join(",");
|
|
609
|
+
if (oldTo !== newTo)
|
|
610
|
+
return false;
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
501
613
|
function diffSchemas(oldState, newSchema) {
|
|
502
614
|
const operations = [];
|
|
503
615
|
const oldTableNames = getTableNamesFromState(oldState);
|
|
@@ -674,6 +786,37 @@ function diffSchemas(oldState, newSchema) {
|
|
|
674
786
|
}
|
|
675
787
|
}
|
|
676
788
|
}
|
|
789
|
+
const oldPolicyNamesByTable = (t) => new Set(Object.keys(t.policies ?? {}));
|
|
790
|
+
const newPolicyListByTable = (t) => t.policies ?? [];
|
|
791
|
+
for (const tableName of commonTableNames) {
|
|
792
|
+
const newTable = newSchema.tables[tableName];
|
|
793
|
+
const oldTable = oldState.tables[tableName];
|
|
794
|
+
if (!newTable || !oldTable)
|
|
795
|
+
continue;
|
|
796
|
+
const oldPolicyNames = oldPolicyNamesByTable(oldTable);
|
|
797
|
+
const newPolicies = newPolicyListByTable(newTable);
|
|
798
|
+
const newPolicyNames = new Set(newPolicies.map((p) => p.name));
|
|
799
|
+
const sortedNewPolicyNames = Array.from(newPolicyNames).sort((a, b) => a.localeCompare(b));
|
|
800
|
+
const sortedOldPolicyNames = Array.from(oldPolicyNames).sort((a, b) => a.localeCompare(b));
|
|
801
|
+
for (const policyName of sortedNewPolicyNames) {
|
|
802
|
+
const policy = newPolicies.find((p) => p.name === policyName);
|
|
803
|
+
if (!policy)
|
|
804
|
+
continue;
|
|
805
|
+
if (!oldPolicyNames.has(policyName)) {
|
|
806
|
+
operations.push({ kind: "create_policy", tableName, policy });
|
|
807
|
+
} else {
|
|
808
|
+
const oldP = oldTable.policies?.[policyName];
|
|
809
|
+
if (oldP && !policyEquals(oldP, policy)) {
|
|
810
|
+
operations.push({ kind: "modify_policy", tableName, policyName, policy });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
for (const policyName of sortedOldPolicyNames) {
|
|
815
|
+
if (!newPolicyNames.has(policyName)) {
|
|
816
|
+
operations.push({ kind: "drop_policy", tableName, policyName });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
677
820
|
for (const tableName of sortedOldTableNames) {
|
|
678
821
|
if (!newTableNames.has(tableName)) {
|
|
679
822
|
operations.push({
|
|
@@ -774,6 +917,7 @@ function validateSchema(schema) {
|
|
|
774
917
|
const table = schema.tables[tableName];
|
|
775
918
|
validateTableColumns(tableName, table, schema.tables);
|
|
776
919
|
}
|
|
920
|
+
validatePolicies(schema);
|
|
777
921
|
}
|
|
778
922
|
function validateDuplicateTables(schema) {
|
|
779
923
|
const tableNames = Object.keys(schema.tables);
|
|
@@ -829,10 +973,37 @@ function validateTableColumns(tableName, table, allTables) {
|
|
|
829
973
|
}
|
|
830
974
|
}
|
|
831
975
|
}
|
|
832
|
-
|
|
976
|
+
function validatePolicies(schema) {
|
|
977
|
+
for (const tableName in schema.tables) {
|
|
978
|
+
const table = schema.tables[tableName];
|
|
979
|
+
if (!table.policies?.length)
|
|
980
|
+
continue;
|
|
981
|
+
for (const policy of table.policies) {
|
|
982
|
+
if (!schema.tables[policy.table]) {
|
|
983
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": referenced table "${policy.table}" does not exist`);
|
|
984
|
+
}
|
|
985
|
+
if (!VALID_POLICY_COMMANDS.includes(policy.command)) {
|
|
986
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": invalid command "${policy.command}". Expected: select, insert, update, delete, or all`);
|
|
987
|
+
}
|
|
988
|
+
const hasUsing = policy.using !== void 0 && String(policy.using).trim() !== "";
|
|
989
|
+
const hasWithCheck = policy.withCheck !== void 0 && String(policy.withCheck).trim() !== "";
|
|
990
|
+
if (!hasUsing && !hasWithCheck) {
|
|
991
|
+
throw new Error(`Policy "${policy.name}" on table "${tableName}": must have at least one of "using" or "with check"`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
var VALID_POLICY_COMMANDS, VALID_BASE_COLUMN_TYPES;
|
|
833
997
|
var init_validator = __esm({
|
|
834
998
|
"node_modules/@xubylele/schema-forge-core/dist/core/validator.js"() {
|
|
835
999
|
"use strict";
|
|
1000
|
+
VALID_POLICY_COMMANDS = [
|
|
1001
|
+
"select",
|
|
1002
|
+
"insert",
|
|
1003
|
+
"update",
|
|
1004
|
+
"delete",
|
|
1005
|
+
"all"
|
|
1006
|
+
];
|
|
836
1007
|
VALID_BASE_COLUMN_TYPES = [
|
|
837
1008
|
"uuid",
|
|
838
1009
|
"varchar",
|
|
@@ -926,6 +1097,12 @@ function classifyOperation(operation) {
|
|
|
926
1097
|
return "DESTRUCTIVE";
|
|
927
1098
|
case "add_primary_key_constraint":
|
|
928
1099
|
return "SAFE";
|
|
1100
|
+
case "create_policy":
|
|
1101
|
+
return "SAFE";
|
|
1102
|
+
case "drop_policy":
|
|
1103
|
+
return "DESTRUCTIVE";
|
|
1104
|
+
case "modify_policy":
|
|
1105
|
+
return "WARNING";
|
|
929
1106
|
default:
|
|
930
1107
|
const _exhaustive = operation;
|
|
931
1108
|
return _exhaustive;
|
|
@@ -1048,6 +1225,22 @@ function checkOperationSafety(operation) {
|
|
|
1048
1225
|
message: "Primary key constraint removed",
|
|
1049
1226
|
operationKind: operation.kind
|
|
1050
1227
|
};
|
|
1228
|
+
case "drop_policy":
|
|
1229
|
+
return {
|
|
1230
|
+
safetyLevel,
|
|
1231
|
+
code: "DROP_POLICY",
|
|
1232
|
+
table: operation.tableName,
|
|
1233
|
+
message: "Policy removed",
|
|
1234
|
+
operationKind: operation.kind
|
|
1235
|
+
};
|
|
1236
|
+
case "modify_policy":
|
|
1237
|
+
return {
|
|
1238
|
+
safetyLevel,
|
|
1239
|
+
code: "MODIFY_POLICY",
|
|
1240
|
+
table: operation.tableName,
|
|
1241
|
+
message: "Policy expression changed",
|
|
1242
|
+
operationKind: operation.kind
|
|
1243
|
+
};
|
|
1051
1244
|
default:
|
|
1052
1245
|
return null;
|
|
1053
1246
|
}
|
|
@@ -1222,9 +1415,21 @@ async function schemaToState(schema) {
|
|
|
1222
1415
|
...column.foreignKey !== void 0 && { foreignKey: column.foreignKey }
|
|
1223
1416
|
};
|
|
1224
1417
|
}
|
|
1418
|
+
let policies;
|
|
1419
|
+
if (table.policies?.length) {
|
|
1420
|
+
policies = {};
|
|
1421
|
+
for (const p of table.policies) {
|
|
1422
|
+
policies[p.name] = {
|
|
1423
|
+
command: p.command,
|
|
1424
|
+
...p.using !== void 0 && { using: p.using },
|
|
1425
|
+
...p.withCheck !== void 0 && { withCheck: p.withCheck }
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1225
1429
|
tables[tableName] = {
|
|
1226
1430
|
columns,
|
|
1227
|
-
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn }
|
|
1431
|
+
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn },
|
|
1432
|
+
...policies !== void 0 && { policies }
|
|
1228
1433
|
};
|
|
1229
1434
|
}
|
|
1230
1435
|
return {
|
|
@@ -1259,9 +1464,20 @@ var init_state_manager = __esm({
|
|
|
1259
1464
|
});
|
|
1260
1465
|
|
|
1261
1466
|
// node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js
|
|
1467
|
+
function generateEnableRls(tableName) {
|
|
1468
|
+
return `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`;
|
|
1469
|
+
}
|
|
1262
1470
|
function generateSql(diff, provider, sqlConfig) {
|
|
1263
1471
|
const statements = [];
|
|
1472
|
+
const enabledRlsTables = /* @__PURE__ */ new Set();
|
|
1264
1473
|
for (const operation of diff.operations) {
|
|
1474
|
+
if (operation.kind === "create_policy" || operation.kind === "modify_policy") {
|
|
1475
|
+
const tableName = operation.tableName;
|
|
1476
|
+
if (!enabledRlsTables.has(tableName)) {
|
|
1477
|
+
statements.push(generateEnableRls(tableName));
|
|
1478
|
+
enabledRlsTables.add(tableName);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1265
1481
|
const sql = generateOperation(operation, provider, sqlConfig);
|
|
1266
1482
|
if (sql) {
|
|
1267
1483
|
statements.push(sql);
|
|
@@ -1291,6 +1507,12 @@ function generateOperation(operation, provider, sqlConfig) {
|
|
|
1291
1507
|
return generateDropPrimaryKeyConstraint(operation.tableName);
|
|
1292
1508
|
case "add_primary_key_constraint":
|
|
1293
1509
|
return generateAddPrimaryKeyConstraint(operation.tableName, operation.columnName);
|
|
1510
|
+
case "create_policy":
|
|
1511
|
+
return generateCreatePolicy(operation.tableName, operation.policy);
|
|
1512
|
+
case "drop_policy":
|
|
1513
|
+
return generateDropPolicy(operation.tableName, operation.policyName);
|
|
1514
|
+
case "modify_policy":
|
|
1515
|
+
return generateModifyPolicy(operation.tableName, operation.policyName, operation.policy);
|
|
1294
1516
|
}
|
|
1295
1517
|
}
|
|
1296
1518
|
function generateCreateTable(table, provider, sqlConfig) {
|
|
@@ -1371,6 +1593,28 @@ function generateAlterColumnNullability(tableName, columnName, toNullable) {
|
|
|
1371
1593
|
}
|
|
1372
1594
|
return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET NOT NULL;`;
|
|
1373
1595
|
}
|
|
1596
|
+
function generateCreatePolicy(tableName, policy) {
|
|
1597
|
+
const command = policy.command === "all" ? "ALL" : policy.command.toUpperCase();
|
|
1598
|
+
const parts = [`CREATE POLICY "${policy.name}" ON ${tableName} FOR ${command}`];
|
|
1599
|
+
if (policy.to !== void 0 && policy.to.length > 0) {
|
|
1600
|
+
parts.push(`TO ${policy.to.join(", ")}`);
|
|
1601
|
+
}
|
|
1602
|
+
if (policy.using !== void 0 && policy.using !== "") {
|
|
1603
|
+
parts.push(`USING (${policy.using})`);
|
|
1604
|
+
}
|
|
1605
|
+
if (policy.withCheck !== void 0 && policy.withCheck !== "") {
|
|
1606
|
+
parts.push(`WITH CHECK (${policy.withCheck})`);
|
|
1607
|
+
}
|
|
1608
|
+
return parts.join(" ") + ";";
|
|
1609
|
+
}
|
|
1610
|
+
function generateDropPolicy(tableName, policyName) {
|
|
1611
|
+
return `DROP POLICY "${policyName}" ON ${tableName};`;
|
|
1612
|
+
}
|
|
1613
|
+
function generateModifyPolicy(tableName, policyName, policy) {
|
|
1614
|
+
const drop = generateDropPolicy(tableName, policyName);
|
|
1615
|
+
const create = generateCreatePolicy(tableName, policy);
|
|
1616
|
+
return drop + "\n\n" + create;
|
|
1617
|
+
}
|
|
1374
1618
|
var init_sql_generator = __esm({
|
|
1375
1619
|
"node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js"() {
|
|
1376
1620
|
"use strict";
|
|
@@ -1924,6 +2168,100 @@ function parseDropTable(stmt) {
|
|
|
1924
2168
|
table: normalizeIdentifier(match[1])
|
|
1925
2169
|
};
|
|
1926
2170
|
}
|
|
2171
|
+
function parseEnableRls(stmt) {
|
|
2172
|
+
const s = stmt.replace(/\s+/g, " ").trim();
|
|
2173
|
+
const match = s.match(/^alter\s+table\s+(\S+)\s+enable\s+row\s+level\s+security\s*;?$/i);
|
|
2174
|
+
if (!match) {
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
return {
|
|
2178
|
+
kind: "ENABLE_RLS",
|
|
2179
|
+
table: normalizeIdentifier(match[1])
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
function extractBalancedParen(s, start) {
|
|
2183
|
+
if (start < 0 || s[start] !== "(") {
|
|
2184
|
+
return null;
|
|
2185
|
+
}
|
|
2186
|
+
let depth = 1;
|
|
2187
|
+
let i = start + 1;
|
|
2188
|
+
while (i < s.length && depth > 0) {
|
|
2189
|
+
const c = s[i];
|
|
2190
|
+
if (c === "'" && (i === 0 || s[i - 1] !== "\\")) {
|
|
2191
|
+
i++;
|
|
2192
|
+
while (i < s.length && (s[i] !== "'" || s[i - 1] === "\\")) {
|
|
2193
|
+
i++;
|
|
2194
|
+
}
|
|
2195
|
+
i++;
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
if (c === "(") {
|
|
2199
|
+
depth++;
|
|
2200
|
+
} else if (c === ")") {
|
|
2201
|
+
depth--;
|
|
2202
|
+
}
|
|
2203
|
+
i++;
|
|
2204
|
+
}
|
|
2205
|
+
if (depth !== 0) {
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
return { content: s.slice(start + 1, i - 1).trim(), endIndex: i };
|
|
2209
|
+
}
|
|
2210
|
+
function parseCreatePolicy(stmt) {
|
|
2211
|
+
const s = stmt.replace(/\s+/g, " ").trim();
|
|
2212
|
+
const quotedMatch = s.match(/^create\s+policy\s+"([^"]+)"\s+on\s+(\S+)\s+for\s+(all|select|insert|update|delete)/i);
|
|
2213
|
+
const unquotedMatch = quotedMatch ? null : s.match(/^create\s+policy\s+(\S+)\s+on\s+(\S+)\s+for\s+(all|select|insert|update|delete)/i);
|
|
2214
|
+
const match = quotedMatch ?? unquotedMatch;
|
|
2215
|
+
if (!match) {
|
|
2216
|
+
return null;
|
|
2217
|
+
}
|
|
2218
|
+
const name = match[1];
|
|
2219
|
+
const table = normalizeIdentifier(match[2]);
|
|
2220
|
+
const command = match[3].toLowerCase();
|
|
2221
|
+
let rest = s.slice(match[0].length).trim();
|
|
2222
|
+
let toRoles;
|
|
2223
|
+
const toMatch = rest.match(/^\s*to\s+/i) ?? rest.match(/\s+to\s+/i);
|
|
2224
|
+
if (toMatch) {
|
|
2225
|
+
const toIdx = toMatch.index;
|
|
2226
|
+
const afterTo = rest.slice(toIdx + toMatch[0].length);
|
|
2227
|
+
const usingStart = afterTo.toLowerCase().indexOf(" using (");
|
|
2228
|
+
const withCheckStart = afterTo.toLowerCase().indexOf(" with check (");
|
|
2229
|
+
const end = usingStart >= 0 && withCheckStart >= 0 ? Math.min(usingStart, withCheckStart) : usingStart >= 0 ? usingStart : withCheckStart >= 0 ? withCheckStart : afterTo.length;
|
|
2230
|
+
const toPart = afterTo.slice(0, end).trim();
|
|
2231
|
+
if (toPart) {
|
|
2232
|
+
toRoles = toPart.split(/[\s,]+/).map((r) => r.trim()).filter(Boolean);
|
|
2233
|
+
}
|
|
2234
|
+
rest = rest.slice(0, toIdx) + rest.slice(toIdx + toMatch[0].length + end);
|
|
2235
|
+
}
|
|
2236
|
+
let using;
|
|
2237
|
+
let withCheck;
|
|
2238
|
+
const usingIdx = rest.toLowerCase().indexOf("using (");
|
|
2239
|
+
if (usingIdx !== -1) {
|
|
2240
|
+
const openParen = rest.indexOf("(", usingIdx);
|
|
2241
|
+
const parsed = extractBalancedParen(rest, openParen);
|
|
2242
|
+
if (parsed) {
|
|
2243
|
+
using = parsed.content;
|
|
2244
|
+
rest = rest.slice(parsed.endIndex).trim();
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const withCheckIdx = rest.toLowerCase().indexOf("with check (");
|
|
2248
|
+
if (withCheckIdx !== -1) {
|
|
2249
|
+
const openParen = rest.indexOf("(", withCheckIdx);
|
|
2250
|
+
const parsed = extractBalancedParen(rest, openParen);
|
|
2251
|
+
if (parsed) {
|
|
2252
|
+
withCheck = parsed.content;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return {
|
|
2256
|
+
kind: "CREATE_POLICY",
|
|
2257
|
+
table,
|
|
2258
|
+
name,
|
|
2259
|
+
command,
|
|
2260
|
+
...using !== void 0 && using !== "" && { using },
|
|
2261
|
+
...withCheck !== void 0 && withCheck !== "" && { withCheck },
|
|
2262
|
+
...toRoles !== void 0 && toRoles.length > 0 && { to: toRoles }
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
1927
2265
|
function parseMigrationSql(sql) {
|
|
1928
2266
|
const statements = splitSqlStatements(sql);
|
|
1929
2267
|
const ops = [];
|
|
@@ -1975,7 +2313,9 @@ var init_parse_migration = __esm({
|
|
|
1975
2313
|
parseSetDropDefault,
|
|
1976
2314
|
parseAddDropConstraint,
|
|
1977
2315
|
parseDropColumn,
|
|
1978
|
-
parseDropTable
|
|
2316
|
+
parseDropTable,
|
|
2317
|
+
parseEnableRls,
|
|
2318
|
+
parseCreatePolicy
|
|
1979
2319
|
];
|
|
1980
2320
|
}
|
|
1981
2321
|
});
|
|
@@ -2149,6 +2489,31 @@ function applySqlOps(ops) {
|
|
|
2149
2489
|
delete tables[op.table];
|
|
2150
2490
|
break;
|
|
2151
2491
|
}
|
|
2492
|
+
case "ENABLE_RLS": {
|
|
2493
|
+
getOrCreateTable(tables, op.table);
|
|
2494
|
+
break;
|
|
2495
|
+
}
|
|
2496
|
+
case "CREATE_POLICY": {
|
|
2497
|
+
const table = getOrCreateTable(tables, op.table);
|
|
2498
|
+
if (!table.policies) {
|
|
2499
|
+
table.policies = [];
|
|
2500
|
+
}
|
|
2501
|
+
const policy = {
|
|
2502
|
+
name: op.name,
|
|
2503
|
+
table: op.table,
|
|
2504
|
+
command: op.command,
|
|
2505
|
+
...op.using !== void 0 && { using: op.using },
|
|
2506
|
+
...op.withCheck !== void 0 && { withCheck: op.withCheck },
|
|
2507
|
+
...op.to !== void 0 && op.to.length > 0 && { to: op.to }
|
|
2508
|
+
};
|
|
2509
|
+
const existing = table.policies.findIndex((p) => p.name === op.name);
|
|
2510
|
+
if (existing >= 0) {
|
|
2511
|
+
table.policies[existing] = policy;
|
|
2512
|
+
} else {
|
|
2513
|
+
table.policies.push(policy);
|
|
2514
|
+
}
|
|
2515
|
+
break;
|
|
2516
|
+
}
|
|
2152
2517
|
}
|
|
2153
2518
|
}
|
|
2154
2519
|
const schema = { tables };
|
|
@@ -2177,17 +2542,39 @@ function renderColumn(column) {
|
|
|
2177
2542
|
}
|
|
2178
2543
|
return ` ${parts.join(" ")}`;
|
|
2179
2544
|
}
|
|
2545
|
+
function renderPolicy(policy) {
|
|
2546
|
+
const lines = [
|
|
2547
|
+
`policy "${policy.name}" on ${policy.table}`,
|
|
2548
|
+
`for ${policy.command}`
|
|
2549
|
+
];
|
|
2550
|
+
if (policy.to !== void 0 && policy.to.length > 0) {
|
|
2551
|
+
lines.push(`to ${policy.to.join(" ")}`);
|
|
2552
|
+
}
|
|
2553
|
+
if (policy.using !== void 0 && policy.using !== "") {
|
|
2554
|
+
lines.push(`using ${policy.using}`);
|
|
2555
|
+
}
|
|
2556
|
+
if (policy.withCheck !== void 0 && policy.withCheck !== "") {
|
|
2557
|
+
lines.push(`with check ${policy.withCheck}`);
|
|
2558
|
+
}
|
|
2559
|
+
return lines.join("\n");
|
|
2560
|
+
}
|
|
2180
2561
|
function schemaToDsl(schema) {
|
|
2181
2562
|
const tableNames = Object.keys(schema.tables).sort((left, right) => left.localeCompare(right));
|
|
2182
|
-
const blocks =
|
|
2563
|
+
const blocks = [];
|
|
2564
|
+
for (const tableName of tableNames) {
|
|
2183
2565
|
const table = schema.tables[tableName];
|
|
2184
|
-
const
|
|
2566
|
+
const tableLines = [`table ${table.name} {`];
|
|
2185
2567
|
for (const column of table.columns) {
|
|
2186
|
-
|
|
2568
|
+
tableLines.push(renderColumn(column));
|
|
2187
2569
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2570
|
+
tableLines.push("}");
|
|
2571
|
+
blocks.push(tableLines.join("\n"));
|
|
2572
|
+
if (table.policies?.length) {
|
|
2573
|
+
for (const policy of table.policies) {
|
|
2574
|
+
blocks.push(renderPolicy(policy));
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2191
2578
|
if (blocks.length === 0) {
|
|
2192
2579
|
return "# SchemaForge schema definition\n";
|
|
2193
2580
|
}
|
|
@@ -2680,9 +3067,11 @@ __export(dist_exports, {
|
|
|
2680
3067
|
parseAddDropConstraint: () => parseAddDropConstraint,
|
|
2681
3068
|
parseAlterColumnType: () => parseAlterColumnType,
|
|
2682
3069
|
parseAlterTableAddColumn: () => parseAlterTableAddColumn,
|
|
3070
|
+
parseCreatePolicy: () => parseCreatePolicy,
|
|
2683
3071
|
parseCreateTable: () => parseCreateTable,
|
|
2684
3072
|
parseDropColumn: () => parseDropColumn,
|
|
2685
3073
|
parseDropTable: () => parseDropTable,
|
|
3074
|
+
parseEnableRls: () => parseEnableRls,
|
|
2686
3075
|
parseMigrationSql: () => parseMigrationSql,
|
|
2687
3076
|
parseSchema: () => parseSchema,
|
|
2688
3077
|
parseSetDropDefault: () => parseSetDropDefault,
|
|
@@ -2733,7 +3122,7 @@ var import_commander8 = require("commander");
|
|
|
2733
3122
|
// package.json
|
|
2734
3123
|
var package_default = {
|
|
2735
3124
|
name: "@xubylele/schema-forge",
|
|
2736
|
-
version: "1.
|
|
3125
|
+
version: "1.12.1",
|
|
2737
3126
|
description: "Universal migration generator from schema DSL",
|
|
2738
3127
|
main: "dist/cli.js",
|
|
2739
3128
|
type: "commonjs",
|
|
@@ -2787,13 +3176,14 @@ var package_default = {
|
|
|
2787
3176
|
boxen: "^8.0.1",
|
|
2788
3177
|
chalk: "^5.6.2",
|
|
2789
3178
|
commander: "^14.0.3",
|
|
2790
|
-
pg: "^8.19.0"
|
|
3179
|
+
pg: "^8.19.0",
|
|
3180
|
+
"update-notifier": "^7.3.1"
|
|
2791
3181
|
},
|
|
2792
3182
|
devDependencies: {
|
|
2793
3183
|
"@changesets/cli": "^2.30.0",
|
|
2794
3184
|
"@types/node": "^25.2.3",
|
|
2795
3185
|
"@types/pg": "^8.18.0",
|
|
2796
|
-
"@xubylele/schema-forge-core": "^1.
|
|
3186
|
+
"@xubylele/schema-forge-core": "^1.5.0",
|
|
2797
3187
|
testcontainers: "^11.8.1",
|
|
2798
3188
|
"ts-node": "^10.9.2",
|
|
2799
3189
|
tsup: "^8.5.1",
|
|
@@ -2882,13 +3272,9 @@ function getStatePath(root, config) {
|
|
|
2882
3272
|
|
|
2883
3273
|
// src/utils/exitCodes.ts
|
|
2884
3274
|
var EXIT_CODES = {
|
|
2885
|
-
/** Successful operation */
|
|
2886
3275
|
SUCCESS: 0,
|
|
2887
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
2888
3276
|
VALIDATION_ERROR: 1,
|
|
2889
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
2890
3277
|
DRIFT_DETECTED: 2,
|
|
2891
|
-
/** Destructive operation detected in CI environment without --force */
|
|
2892
3278
|
CI_DESTRUCTIVE: 3
|
|
2893
3279
|
};
|
|
2894
3280
|
function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
|
|
@@ -3739,6 +4125,35 @@ function getCliMetaPath() {
|
|
|
3739
4125
|
function getReleaseUrl(version) {
|
|
3740
4126
|
return `https://github.com/xubylele/schema-forge/releases/tag/v${version}`;
|
|
3741
4127
|
}
|
|
4128
|
+
function extractChangelogSection(changelogText, version) {
|
|
4129
|
+
const heading = `## ${version}`;
|
|
4130
|
+
const idx = changelogText.indexOf(heading);
|
|
4131
|
+
if (idx === -1) {
|
|
4132
|
+
return null;
|
|
4133
|
+
}
|
|
4134
|
+
const start = idx + heading.length;
|
|
4135
|
+
const rest = changelogText.slice(start);
|
|
4136
|
+
const nextHeading = rest.indexOf("\n## ");
|
|
4137
|
+
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
|
4138
|
+
return section.trim() || null;
|
|
4139
|
+
}
|
|
4140
|
+
async function fetchChangelogForVersion(version) {
|
|
4141
|
+
const urls = [
|
|
4142
|
+
`https://raw.githubusercontent.com/xubylele/schema-forge/v${version}/CHANGELOG.md`,
|
|
4143
|
+
"https://raw.githubusercontent.com/xubylele/schema-forge/main/CHANGELOG.md"
|
|
4144
|
+
];
|
|
4145
|
+
for (const url of urls) {
|
|
4146
|
+
try {
|
|
4147
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
4148
|
+
if (!res.ok) continue;
|
|
4149
|
+
const text = await res.text();
|
|
4150
|
+
const section = extractChangelogSection(text, version);
|
|
4151
|
+
if (section) return section;
|
|
4152
|
+
} catch {
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
return null;
|
|
4156
|
+
}
|
|
3742
4157
|
function shouldShowWhatsNew(argv) {
|
|
3743
4158
|
if (argv.length === 0) {
|
|
3744
4159
|
return false;
|
|
@@ -3758,7 +4173,14 @@ async function showWhatsNewIfUpdated(currentVersion, argv) {
|
|
|
3758
4173
|
if (meta.lastSeenVersion === currentVersion) {
|
|
3759
4174
|
return;
|
|
3760
4175
|
}
|
|
3761
|
-
|
|
4176
|
+
const section = await fetchChangelogForVersion(currentVersion);
|
|
4177
|
+
if (section) {
|
|
4178
|
+
info(`What's new in schema-forge v${currentVersion}:`);
|
|
4179
|
+
info(section);
|
|
4180
|
+
info(`Release: ${getReleaseUrl(currentVersion)}`);
|
|
4181
|
+
} else {
|
|
4182
|
+
info(`What's new in schema-forge v${currentVersion}: ${getReleaseUrl(currentVersion)}`);
|
|
4183
|
+
}
|
|
3762
4184
|
await writeJsonFile(metaPath, { lastSeenVersion: currentVersion });
|
|
3763
4185
|
} catch {
|
|
3764
4186
|
}
|
|
@@ -3857,10 +4279,21 @@ program.command("validate").description("Detect destructive or risky schema chan
|
|
|
3857
4279
|
await handleError(error2);
|
|
3858
4280
|
}
|
|
3859
4281
|
});
|
|
4282
|
+
function shouldCheckForUpdate(argv) {
|
|
4283
|
+
if (process.env.CI === "true") {
|
|
4284
|
+
return false;
|
|
4285
|
+
}
|
|
4286
|
+
const onlyHelpOrVersion = argv.length === 0 || argv.length === 1 && ["--help", "-h", "--version", "-V"].includes(argv[0]);
|
|
4287
|
+
return !onlyHelpOrVersion;
|
|
4288
|
+
}
|
|
3860
4289
|
async function main() {
|
|
3861
4290
|
const argv = process.argv.slice(2);
|
|
3862
4291
|
await seedLastSeenVersion(package_default.version);
|
|
3863
4292
|
await showWhatsNewIfUpdated(package_default.version, argv);
|
|
4293
|
+
if (shouldCheckForUpdate(argv)) {
|
|
4294
|
+
import("update-notifier").then((m) => m.default({ pkg: package_default, shouldNotifyInNpmScript: false }).notify()).catch(() => {
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
3864
4297
|
program.parse(process.argv);
|
|
3865
4298
|
if (!argv.length) {
|
|
3866
4299
|
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.1",
|
|
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.5.0",
|
|
64
65
|
"testcontainers": "^11.8.1",
|
|
65
66
|
"ts-node": "^10.9.2",
|
|
66
67
|
"tsup": "^8.5.1",
|