@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.
- package/README.md +17 -4
- package/dist/api.js +185 -14
- package/dist/cli.js +187 -16
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2563
|
+
const blocks = [];
|
|
2564
|
+
for (const tableName of tableNames) {
|
|
2401
2565
|
const table = schema.tables[tableName];
|
|
2402
|
-
const
|
|
2566
|
+
const tableLines = [`table ${table.name} {`];
|
|
2403
2567
|
for (const column of table.columns) {
|
|
2404
|
-
|
|
2568
|
+
tableLines.push(renderColumn(column));
|
|
2405
2569
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2563
|
+
const blocks = [];
|
|
2564
|
+
for (const tableName of tableNames) {
|
|
2401
2565
|
const table = schema.tables[tableName];
|
|
2402
|
-
const
|
|
2566
|
+
const tableLines = [`table ${table.name} {`];
|
|
2403
2567
|
for (const column of table.columns) {
|
|
2404
|
-
|
|
2568
|
+
tableLines.push(renderColumn(column));
|
|
2405
2569
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|