@xubylele/schema-forge 1.12.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.
Files changed (4) hide show
  1. package/README.md +17 -4
  2. package/dist/api.js +185 -14
  3. package/dist/cli.js +187 -16
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -14,7 +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
+ - **Policy (RLS) support** - Define Row Level Security policies in the DSL: `for select|insert|update|delete|all`, optional `to role1 role2` (e.g. `anon`, `authenticated`). Invalid policies produce clear CLI errors during validation. See [RLS policy patterns](https://github.com/xubylele/schema-forge-core/blob/main/docs/rls-policy-patterns.md) for user-owned rows, public read/authenticated write, and multi-tenant examples.
18
18
 
19
19
  ## Installation
20
20
 
@@ -325,7 +325,7 @@ Live `--json` output returns a structured `DriftReport`:
325
325
 
326
326
  Validation checks include:
327
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).
328
+ - **Policy validation** – Each policy must reference an existing table, use a valid command (`select` / `insert` / `update` / `delete` / `all`), and have at least one of `using` or `with check`. Invalid policies cause validation to fail with a clear error (exit code 1).
329
329
  - Dropped tables (`DROP_TABLE`, error)
330
330
  - Dropped columns (`DROP_COLUMN`, error)
331
331
  - Column type changes (`ALTER_COLUMN_TYPE`, warning/error based on compatibility heuristics)
@@ -584,9 +584,22 @@ using auth.uid() = id
584
584
  ```
585
585
 
586
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>`
587
+ - **Continuation:** `for <command>` (required): `select`, `insert`, `update`, `delete`, or `all` (applies to all commands). Optional `to role1 role2` (e.g. `to anon authenticated`). Optional `using <expr>` and `with check <expr>`.
588
588
 
589
- Invalid policies (missing table, invalid command, or no expressions) fail `schema-forge validate` with a clear error message.
589
+ Example with `for all` and `to`:
590
+
591
+ ```sql
592
+ policy "Own rows" on profiles
593
+ for all
594
+ using auth.uid() = id
595
+ with check auth.uid() = id
596
+
597
+ policy "Public read" on items
598
+ for select to anon authenticated
599
+ using true
600
+ ```
601
+
602
+ Invalid policies (missing table, invalid command, or no expressions) fail `schema-forge validate` with a clear error message. For common patterns (user-owned rows, public read / authenticated write, multi-tenant), see [RLS policy patterns](https://github.com/xubylele/schema-forge-core/blob/main/docs/rls-policy-patterns.md) in the core package.
590
603
 
591
604
  ### Examples
592
605
 
package/dist/api.js CHANGED
@@ -271,6 +271,7 @@ function parseSchema(source) {
271
271
  let command;
272
272
  let using;
273
273
  let withCheck;
274
+ let toRoles;
274
275
  let lineIdx = startLine + 1;
275
276
  while (lineIdx < lines.length) {
276
277
  const cleaned = cleanLine(lines[lineIdx]);
@@ -279,14 +280,30 @@ function parseSchema(source) {
279
280
  continue;
280
281
  }
281
282
  if (cleaned.startsWith("for ")) {
282
- const cmd = cleaned.slice(4).trim().toLowerCase();
283
+ const rest = cleaned.slice(4).trim().toLowerCase();
284
+ const parts = rest.split(/\s+/);
285
+ const cmd = parts[0];
283
286
  if (!POLICY_COMMANDS.includes(cmd)) {
284
- throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, or delete`);
287
+ throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, delete, or all`);
285
288
  }
286
289
  if (command !== void 0) {
287
290
  throw new Error(`Line ${lineIdx + 1}: Duplicate 'for' in policy`);
288
291
  }
289
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
+ }
290
307
  lineIdx++;
291
308
  continue;
292
309
  }
@@ -316,7 +333,8 @@ function parseSchema(source) {
316
333
  table: tableIdent,
317
334
  command,
318
335
  ...using !== void 0 && { using },
319
- ...withCheck !== void 0 && { withCheck }
336
+ ...withCheck !== void 0 && { withCheck },
337
+ ...toRoles !== void 0 && toRoles.length > 0 && { to: toRoles }
320
338
  };
321
339
  return { policy, nextLineIndex: lineIdx };
322
340
  }
@@ -396,7 +414,7 @@ var POLICY_COMMANDS;
396
414
  var init_parser = __esm({
397
415
  "node_modules/@xubylele/schema-forge-core/dist/core/parser.js"() {
398
416
  "use strict";
399
- POLICY_COMMANDS = ["select", "insert", "update", "delete"];
417
+ POLICY_COMMANDS = ["select", "insert", "update", "delete", "all"];
400
418
  }
401
419
  });
402
420
 
@@ -586,6 +604,10 @@ function policyEquals(oldP, newP) {
586
604
  return false;
587
605
  if (normalizePolicyExpression(oldP.withCheck) !== normalizePolicyExpression(newP.withCheck))
588
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;
589
611
  return true;
590
612
  }
591
613
  function diffSchemas(oldState, newSchema) {
@@ -961,7 +983,7 @@ function validatePolicies(schema) {
961
983
  throw new Error(`Policy "${policy.name}" on table "${tableName}": referenced table "${policy.table}" does not exist`);
962
984
  }
963
985
  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`);
986
+ throw new Error(`Policy "${policy.name}" on table "${tableName}": invalid command "${policy.command}". Expected: select, insert, update, delete, or all`);
965
987
  }
966
988
  const hasUsing = policy.using !== void 0 && String(policy.using).trim() !== "";
967
989
  const hasWithCheck = policy.withCheck !== void 0 && String(policy.withCheck).trim() !== "";
@@ -979,7 +1001,8 @@ var init_validator = __esm({
979
1001
  "select",
980
1002
  "insert",
981
1003
  "update",
982
- "delete"
1004
+ "delete",
1005
+ "all"
983
1006
  ];
984
1007
  VALID_BASE_COLUMN_TYPES = [
985
1008
  "uuid",
@@ -1571,8 +1594,11 @@ function generateAlterColumnNullability(tableName, columnName, toNullable) {
1571
1594
  return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET NOT NULL;`;
1572
1595
  }
1573
1596
  function generateCreatePolicy(tableName, policy) {
1574
- const command = policy.command.toUpperCase();
1597
+ const command = policy.command === "all" ? "ALL" : policy.command.toUpperCase();
1575
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
+ }
1576
1602
  if (policy.using !== void 0 && policy.using !== "") {
1577
1603
  parts.push(`USING (${policy.using})`);
1578
1604
  }
@@ -2142,6 +2168,100 @@ function parseDropTable(stmt) {
2142
2168
  table: normalizeIdentifier(match[1])
2143
2169
  };
2144
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
+ }
2145
2265
  function parseMigrationSql(sql) {
2146
2266
  const statements = splitSqlStatements(sql);
2147
2267
  const ops = [];
@@ -2193,7 +2313,9 @@ var init_parse_migration = __esm({
2193
2313
  parseSetDropDefault,
2194
2314
  parseAddDropConstraint,
2195
2315
  parseDropColumn,
2196
- parseDropTable
2316
+ parseDropTable,
2317
+ parseEnableRls,
2318
+ parseCreatePolicy
2197
2319
  ];
2198
2320
  }
2199
2321
  });
@@ -2367,6 +2489,31 @@ function applySqlOps(ops) {
2367
2489
  delete tables[op.table];
2368
2490
  break;
2369
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
+ }
2370
2517
  }
2371
2518
  }
2372
2519
  const schema = { tables };
@@ -2395,17 +2542,39 @@ function renderColumn(column) {
2395
2542
  }
2396
2543
  return ` ${parts.join(" ")}`;
2397
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
+ }
2398
2561
  function schemaToDsl(schema) {
2399
2562
  const tableNames = Object.keys(schema.tables).sort((left, right) => left.localeCompare(right));
2400
- const blocks = tableNames.map((tableName) => {
2563
+ const blocks = [];
2564
+ for (const tableName of tableNames) {
2401
2565
  const table = schema.tables[tableName];
2402
- const lines = [`table ${table.name} {`];
2566
+ const tableLines = [`table ${table.name} {`];
2403
2567
  for (const column of table.columns) {
2404
- lines.push(renderColumn(column));
2568
+ tableLines.push(renderColumn(column));
2405
2569
  }
2406
- lines.push("}");
2407
- return lines.join("\n");
2408
- });
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
+ }
2409
2578
  if (blocks.length === 0) {
2410
2579
  return "# SchemaForge schema definition\n";
2411
2580
  }
@@ -2898,9 +3067,11 @@ __export(dist_exports, {
2898
3067
  parseAddDropConstraint: () => parseAddDropConstraint,
2899
3068
  parseAlterColumnType: () => parseAlterColumnType,
2900
3069
  parseAlterTableAddColumn: () => parseAlterTableAddColumn,
3070
+ parseCreatePolicy: () => parseCreatePolicy,
2901
3071
  parseCreateTable: () => parseCreateTable,
2902
3072
  parseDropColumn: () => parseDropColumn,
2903
3073
  parseDropTable: () => parseDropTable,
3074
+ parseEnableRls: () => parseEnableRls,
2904
3075
  parseMigrationSql: () => parseMigrationSql,
2905
3076
  parseSchema: () => parseSchema,
2906
3077
  parseSetDropDefault: () => parseSetDropDefault,
package/dist/cli.js CHANGED
@@ -271,6 +271,7 @@ function parseSchema(source) {
271
271
  let command;
272
272
  let using;
273
273
  let withCheck;
274
+ let toRoles;
274
275
  let lineIdx = startLine + 1;
275
276
  while (lineIdx < lines.length) {
276
277
  const cleaned = cleanLine(lines[lineIdx]);
@@ -279,14 +280,30 @@ function parseSchema(source) {
279
280
  continue;
280
281
  }
281
282
  if (cleaned.startsWith("for ")) {
282
- const cmd = cleaned.slice(4).trim().toLowerCase();
283
+ const rest = cleaned.slice(4).trim().toLowerCase();
284
+ const parts = rest.split(/\s+/);
285
+ const cmd = parts[0];
283
286
  if (!POLICY_COMMANDS.includes(cmd)) {
284
- throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, or delete`);
287
+ throw new Error(`Line ${lineIdx + 1}: Invalid policy command '${cmd}'. Expected: select, insert, update, delete, or all`);
285
288
  }
286
289
  if (command !== void 0) {
287
290
  throw new Error(`Line ${lineIdx + 1}: Duplicate 'for' in policy`);
288
291
  }
289
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
+ }
290
307
  lineIdx++;
291
308
  continue;
292
309
  }
@@ -316,7 +333,8 @@ function parseSchema(source) {
316
333
  table: tableIdent,
317
334
  command,
318
335
  ...using !== void 0 && { using },
319
- ...withCheck !== void 0 && { withCheck }
336
+ ...withCheck !== void 0 && { withCheck },
337
+ ...toRoles !== void 0 && toRoles.length > 0 && { to: toRoles }
320
338
  };
321
339
  return { policy, nextLineIndex: lineIdx };
322
340
  }
@@ -396,7 +414,7 @@ var POLICY_COMMANDS;
396
414
  var init_parser = __esm({
397
415
  "node_modules/@xubylele/schema-forge-core/dist/core/parser.js"() {
398
416
  "use strict";
399
- POLICY_COMMANDS = ["select", "insert", "update", "delete"];
417
+ POLICY_COMMANDS = ["select", "insert", "update", "delete", "all"];
400
418
  }
401
419
  });
402
420
 
@@ -586,6 +604,10 @@ function policyEquals(oldP, newP) {
586
604
  return false;
587
605
  if (normalizePolicyExpression(oldP.withCheck) !== normalizePolicyExpression(newP.withCheck))
588
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;
589
611
  return true;
590
612
  }
591
613
  function diffSchemas(oldState, newSchema) {
@@ -961,7 +983,7 @@ function validatePolicies(schema) {
961
983
  throw new Error(`Policy "${policy.name}" on table "${tableName}": referenced table "${policy.table}" does not exist`);
962
984
  }
963
985
  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`);
986
+ throw new Error(`Policy "${policy.name}" on table "${tableName}": invalid command "${policy.command}". Expected: select, insert, update, delete, or all`);
965
987
  }
966
988
  const hasUsing = policy.using !== void 0 && String(policy.using).trim() !== "";
967
989
  const hasWithCheck = policy.withCheck !== void 0 && String(policy.withCheck).trim() !== "";
@@ -979,7 +1001,8 @@ var init_validator = __esm({
979
1001
  "select",
980
1002
  "insert",
981
1003
  "update",
982
- "delete"
1004
+ "delete",
1005
+ "all"
983
1006
  ];
984
1007
  VALID_BASE_COLUMN_TYPES = [
985
1008
  "uuid",
@@ -1571,8 +1594,11 @@ function generateAlterColumnNullability(tableName, columnName, toNullable) {
1571
1594
  return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET NOT NULL;`;
1572
1595
  }
1573
1596
  function generateCreatePolicy(tableName, policy) {
1574
- const command = policy.command.toUpperCase();
1597
+ const command = policy.command === "all" ? "ALL" : policy.command.toUpperCase();
1575
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
+ }
1576
1602
  if (policy.using !== void 0 && policy.using !== "") {
1577
1603
  parts.push(`USING (${policy.using})`);
1578
1604
  }
@@ -2142,6 +2168,100 @@ function parseDropTable(stmt) {
2142
2168
  table: normalizeIdentifier(match[1])
2143
2169
  };
2144
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
+ }
2145
2265
  function parseMigrationSql(sql) {
2146
2266
  const statements = splitSqlStatements(sql);
2147
2267
  const ops = [];
@@ -2193,7 +2313,9 @@ var init_parse_migration = __esm({
2193
2313
  parseSetDropDefault,
2194
2314
  parseAddDropConstraint,
2195
2315
  parseDropColumn,
2196
- parseDropTable
2316
+ parseDropTable,
2317
+ parseEnableRls,
2318
+ parseCreatePolicy
2197
2319
  ];
2198
2320
  }
2199
2321
  });
@@ -2367,6 +2489,31 @@ function applySqlOps(ops) {
2367
2489
  delete tables[op.table];
2368
2490
  break;
2369
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
+ }
2370
2517
  }
2371
2518
  }
2372
2519
  const schema = { tables };
@@ -2395,17 +2542,39 @@ function renderColumn(column) {
2395
2542
  }
2396
2543
  return ` ${parts.join(" ")}`;
2397
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
+ }
2398
2561
  function schemaToDsl(schema) {
2399
2562
  const tableNames = Object.keys(schema.tables).sort((left, right) => left.localeCompare(right));
2400
- const blocks = tableNames.map((tableName) => {
2563
+ const blocks = [];
2564
+ for (const tableName of tableNames) {
2401
2565
  const table = schema.tables[tableName];
2402
- const lines = [`table ${table.name} {`];
2566
+ const tableLines = [`table ${table.name} {`];
2403
2567
  for (const column of table.columns) {
2404
- lines.push(renderColumn(column));
2568
+ tableLines.push(renderColumn(column));
2405
2569
  }
2406
- lines.push("}");
2407
- return lines.join("\n");
2408
- });
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
+ }
2409
2578
  if (blocks.length === 0) {
2410
2579
  return "# SchemaForge schema definition\n";
2411
2580
  }
@@ -2898,9 +3067,11 @@ __export(dist_exports, {
2898
3067
  parseAddDropConstraint: () => parseAddDropConstraint,
2899
3068
  parseAlterColumnType: () => parseAlterColumnType,
2900
3069
  parseAlterTableAddColumn: () => parseAlterTableAddColumn,
3070
+ parseCreatePolicy: () => parseCreatePolicy,
2901
3071
  parseCreateTable: () => parseCreateTable,
2902
3072
  parseDropColumn: () => parseDropColumn,
2903
3073
  parseDropTable: () => parseDropTable,
3074
+ parseEnableRls: () => parseEnableRls,
2904
3075
  parseMigrationSql: () => parseMigrationSql,
2905
3076
  parseSchema: () => parseSchema,
2906
3077
  parseSetDropDefault: () => parseSetDropDefault,
@@ -2951,7 +3122,7 @@ var import_commander8 = require("commander");
2951
3122
  // package.json
2952
3123
  var package_default = {
2953
3124
  name: "@xubylele/schema-forge",
2954
- version: "1.12.0",
3125
+ version: "1.12.1",
2955
3126
  description: "Universal migration generator from schema DSL",
2956
3127
  main: "dist/cli.js",
2957
3128
  type: "commonjs",
@@ -3012,7 +3183,7 @@ var package_default = {
3012
3183
  "@changesets/cli": "^2.30.0",
3013
3184
  "@types/node": "^25.2.3",
3014
3185
  "@types/pg": "^8.18.0",
3015
- "@xubylele/schema-forge-core": "^1.4.0",
3186
+ "@xubylele/schema-forge-core": "^1.5.0",
3016
3187
  testcontainers: "^11.8.1",
3017
3188
  "ts-node": "^10.9.2",
3018
3189
  tsup: "^8.5.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xubylele/schema-forge",
3
- "version": "1.12.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",
@@ -61,7 +61,7 @@
61
61
  "@changesets/cli": "^2.30.0",
62
62
  "@types/node": "^25.2.3",
63
63
  "@types/pg": "^8.18.0",
64
- "@xubylele/schema-forge-core": "^1.4.0",
64
+ "@xubylele/schema-forge-core": "^1.5.0",
65
65
  "testcontainers": "^11.8.1",
66
66
  "ts-node": "^10.9.2",
67
67
  "tsup": "^8.5.1",