@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/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
- currentLine++;
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
- var VALID_BASE_COLUMN_TYPES;
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 = tableNames.map((tableName) => {
2563
+ const blocks = [];
2564
+ for (const tableName of tableNames) {
2183
2565
  const table = schema.tables[tableName];
2184
- const lines = [`table ${table.name} {`];
2566
+ const tableLines = [`table ${table.name} {`];
2185
2567
  for (const column of table.columns) {
2186
- lines.push(renderColumn(column));
2568
+ tableLines.push(renderColumn(column));
2187
2569
  }
2188
- lines.push("}");
2189
- return lines.join("\n");
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.11.0",
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.3.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
- info(`What's new in schema-forge v${currentVersion}: ${getReleaseUrl(currentVersion)}`);
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.11.0",
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.3.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",