@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/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,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.`);
|
|
400
|
+
}
|
|
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}"`);
|
|
314
405
|
}
|
|
315
|
-
|
|
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(diff2, provider, sqlConfig) {
|
|
1263
1471
|
const statements = [];
|
|
1472
|
+
const enabledRlsTables = /* @__PURE__ */ new Set();
|
|
1264
1473
|
for (const operation of diff2.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,
|
|
@@ -2941,13 +3330,9 @@ async function withPostgresQueryExecutor(connectionString, run) {
|
|
|
2941
3330
|
|
|
2942
3331
|
// src/utils/exitCodes.ts
|
|
2943
3332
|
var EXIT_CODES = {
|
|
2944
|
-
/** Successful operation */
|
|
2945
3333
|
SUCCESS: 0,
|
|
2946
|
-
/** Validation error (invalid DSL syntax, config errors, missing files, etc.) */
|
|
2947
3334
|
VALIDATION_ERROR: 1,
|
|
2948
|
-
/** Drift detected - Reserved for future use when comparing actual DB state vs schema */
|
|
2949
3335
|
DRIFT_DETECTED: 2,
|
|
2950
|
-
/** Destructive operation detected in CI environment without --force */
|
|
2951
3336
|
CI_DESTRUCTIVE: 3
|
|
2952
3337
|
};
|
|
2953
3338
|
function shouldFailCIDestructive(isCIEnvironment, hasDestructiveFindings2, isForceEnabled) {
|