@supacloud/cli 0.40.0 → 0.41.0
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/index.js +188 -18
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -239502,6 +239502,61 @@ function migrationExecutionStatements(statements) {
|
|
|
239502
239502
|
const hasOuterTransaction = /^(?:BEGIN(?:\s+(?:WORK|TRANSACTION))?|START\s+TRANSACTION)$/i.test(first) && /^(?:COMMIT|END)(?:\s+(?:WORK|TRANSACTION))?$/i.test(last);
|
|
239503
239503
|
return hasOuterTransaction ? statements.slice(1, -1) : [...statements];
|
|
239504
239504
|
}
|
|
239505
|
+
function skipSqlTrivia(sql, start) {
|
|
239506
|
+
let cursor = start;
|
|
239507
|
+
for (;; ) {
|
|
239508
|
+
while (cursor < sql.length && /\s/.test(sql[cursor]))
|
|
239509
|
+
cursor++;
|
|
239510
|
+
if (sql.startsWith("--", cursor)) {
|
|
239511
|
+
cursor = lineCommentEnd(sql, cursor);
|
|
239512
|
+
continue;
|
|
239513
|
+
}
|
|
239514
|
+
if (sql.startsWith("/*", cursor)) {
|
|
239515
|
+
cursor = blockCommentEnd(sql, cursor);
|
|
239516
|
+
continue;
|
|
239517
|
+
}
|
|
239518
|
+
return cursor;
|
|
239519
|
+
}
|
|
239520
|
+
}
|
|
239521
|
+
function doStatementBody(sql, keywordEnd) {
|
|
239522
|
+
let cursor = skipSqlTrivia(sql, keywordEnd);
|
|
239523
|
+
if (/^LANGUAGE\b/i.test(sql.slice(cursor))) {
|
|
239524
|
+
cursor += "LANGUAGE".length;
|
|
239525
|
+
cursor = skipSqlTrivia(sql, cursor);
|
|
239526
|
+
if (sql[cursor] === "'")
|
|
239527
|
+
cursor = maskSingleQuotedString(sql, cursor).end;
|
|
239528
|
+
else if (sql[cursor] === '"')
|
|
239529
|
+
cursor = maskDoubleQuotedIdentifier(sql, cursor).end;
|
|
239530
|
+
else {
|
|
239531
|
+
const languageName = /^[A-Za-z_][A-Za-z0-9_]*/.exec(sql.slice(cursor));
|
|
239532
|
+
if (!languageName)
|
|
239533
|
+
return null;
|
|
239534
|
+
cursor += languageName[0].length;
|
|
239535
|
+
}
|
|
239536
|
+
cursor = skipSqlTrivia(sql, cursor);
|
|
239537
|
+
}
|
|
239538
|
+
if (sql[cursor] === "'") {
|
|
239539
|
+
const end = maskSingleQuotedString(sql, cursor).end;
|
|
239540
|
+
return sql.slice(cursor + 1, Math.max(cursor + 1, end - 1));
|
|
239541
|
+
}
|
|
239542
|
+
const tag = sql[cursor] === "$" ? dollarQuoteTagAt(sql, cursor) : "";
|
|
239543
|
+
if (!tag)
|
|
239544
|
+
return null;
|
|
239545
|
+
const bodyEnd = sql.indexOf(tag, cursor + tag.length);
|
|
239546
|
+
return bodyEnd === -1 ? null : sql.slice(cursor + tag.length, bodyEnd);
|
|
239547
|
+
}
|
|
239548
|
+
function topLevelDoBodies(sql) {
|
|
239549
|
+
const masked = maskSqlPolicyNoise(sql);
|
|
239550
|
+
const doKeywordPattern = /(?:^|;)\s*DO\b/gi;
|
|
239551
|
+
const bodies = [];
|
|
239552
|
+
let match;
|
|
239553
|
+
while ((match = doKeywordPattern.exec(masked)) !== null) {
|
|
239554
|
+
const body = doStatementBody(sql, doKeywordPattern.lastIndex);
|
|
239555
|
+
if (body !== null)
|
|
239556
|
+
bodies.push(body);
|
|
239557
|
+
}
|
|
239558
|
+
return bodies;
|
|
239559
|
+
}
|
|
239505
239560
|
function splitTopLevelClauses(sql) {
|
|
239506
239561
|
const masked = maskSqlNoise(sql);
|
|
239507
239562
|
const clauses = [];
|
|
@@ -239596,14 +239651,6 @@ var RISK_RULES = [
|
|
|
239596
239651
|
description: "Renames table. Causes immediate downtime for application code.",
|
|
239597
239652
|
recommendation: "Follow Expand-Contract: Create a view or alias table during transition."
|
|
239598
239653
|
},
|
|
239599
|
-
{
|
|
239600
|
-
type: "manual_review_do_block",
|
|
239601
|
-
level: "HIGH",
|
|
239602
|
-
pattern: /^\s*DO\b/i,
|
|
239603
|
-
description: "DO blocks can execute dynamic or procedural DDL that static rules cannot inspect safely.",
|
|
239604
|
-
recommendation: "Move schema changes into explicit SQL statements; push_migrations rejects opaque procedural SQL.",
|
|
239605
|
-
blocksTransactionalPush: true
|
|
239606
|
-
},
|
|
239607
239654
|
{
|
|
239608
239655
|
type: "manual_review_procedural_definition",
|
|
239609
239656
|
level: "HIGH",
|
|
@@ -239705,7 +239752,7 @@ var MIGRATION_LEDGER_RELATION_END = String.raw`(?=$|[\s,;(*])`;
|
|
|
239705
239752
|
var MIGRATION_LEDGER_DDL_OR_MAINTENANCE_PREFIX = String.raw`(?:CREATE\s+(?:UNIQUE\s+)?INDEX\b[^;]*\bON\s+(?:ONLY\s+)?|CREATE\s+(?:CONSTRAINT\s+)?TRIGGER\b[^;]*\bON\s+|DROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|CREATE\s+RULE\b[^;]*\bTO\s+|DROP\s+RULE\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|CREATE\s+POLICY\b[^;]*\bON\s+|ALTER\s+POLICY\b[^;]*\bON\s+|DROP\s+POLICY\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|COMMENT\s+ON\s+TABLE\s+|SECURITY\s+LABEL(?:\s+FOR\s+[^;\s]+)?\s+ON\s+TABLE\s+|REINDEX(?:\s*\([^;)]*\))?\s+TABLE\s+(?:CONCURRENTLY\s+)?|CLUSTER(?:\s+VERBOSE)?\s+|VACUUM(?:\s*\([^;)]*\))?(?:\s+(?:FULL|FREEZE|VERBOSE|ANALYZE))*\s+|ANALYZE(?:\s*\([^;)]*\))?(?:\s+VERBOSE)?\s+)`;
|
|
239706
239753
|
var MIGRATION_LEDGER_MODIFICATION_PATTERN = new RegExp(String.raw`\b(?:(?:CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?|INSERT\s+INTO\s+(?:ONLY\s+)?|UPDATE\s+(?:ONLY\s+)?|DELETE\s+FROM\s+(?:ONLY\s+)?|MERGE\s+INTO\s+(?:ONLY\s+)?|ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?|COPY\s+)` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`|(?:DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?|TRUNCATE(?:\s+TABLE)?\s+|LOCK\s+(?:TABLE\s+)?)[^;]*` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`|` + MIGRATION_LEDGER_DDL_OR_MAINTENANCE_PREFIX + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`)`, "i");
|
|
239707
239754
|
var NON_TABLE_PRIVILEGE_TARGET = String.raw`(?:ALL\s+(?:FUNCTIONS|PROCEDURES|ROUTINES|SEQUENCES)\s+IN\s+SCHEMA|DATABASE|DOMAIN|FOREIGN\s+DATA\s+WRAPPER|FOREIGN\s+SERVER|FUNCTION|LANGUAGE|LARGE\s+OBJECT|PARAMETER|PROCEDURE|ROUTINE|SCHEMA|SEQUENCE|TABLESPACE|TYPE)\b`;
|
|
239708
|
-
var MIGRATION_LEDGER_PRIVILEGE_PATTERN = new RegExp(String.raw`\b(?:GRANT|REVOKE)\b[^;]*\bON\s+(?:(?:TABLE\s+)?(?!` + NON_TABLE_PRIVILEGE_TARGET + String.raw`)(?:(?!\b(?:TO|FROM)\b)[^;])*?` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`(?=[^;]*\b(?:TO|FROM)\b)|ALL\s+TABLES\s+IN\s+SCHEMA\s+"?
|
|
239755
|
+
var MIGRATION_LEDGER_PRIVILEGE_PATTERN = new RegExp(String.raw`\b(?:GRANT|REVOKE)\b[^;]*\bON\s+(?:(?:TABLE\s+)?(?!` + NON_TABLE_PRIVILEGE_TARGET + String.raw`)(?:(?!\b(?:TO|FROM)\b)[^;])*?` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`(?=[^;]*\b(?:TO|FROM)\b)|ALL\s+TABLES\s+IN\s+SCHEMA\s+"?supabase_migrations"?(?=$|[\s,;]))`, "i");
|
|
239709
239756
|
var PUSH_BLOCKER_RULES = [
|
|
239710
239757
|
{
|
|
239711
239758
|
type: "unsupported_project_scope_management",
|
|
@@ -239832,6 +239879,7 @@ var MIGRATION_LEDGER_BLOCKER_RULES = [
|
|
|
239832
239879
|
blocksTransactionalPush: true
|
|
239833
239880
|
}
|
|
239834
239881
|
];
|
|
239882
|
+
var DO_BODY_BLOCKER_RULES = PUSH_BLOCKER_RULES.filter((rule) => rule.type !== "unsupported_transaction_control");
|
|
239835
239883
|
function matchingRisks(rawStatement, maskedStatement, rules) {
|
|
239836
239884
|
return rules.filter((rule) => rule.pattern.test(maskedStatement) && !rule.excludePattern?.test(maskedStatement)).map((rule) => ({
|
|
239837
239885
|
level: rule.level,
|
|
@@ -239908,6 +239956,13 @@ function analyzeMigrationSql(sql) {
|
|
|
239908
239956
|
risks.push(...matchingRisks(rawStatement, masked, PUSH_BLOCKER_RULES));
|
|
239909
239957
|
risks.push(...matchingRisks(rawStatement, policyMasked, QUOTED_FUNCTION_BLOCKER_RULES));
|
|
239910
239958
|
risks.push(...matchingRisks(rawStatement, policyMasked, MIGRATION_LEDGER_BLOCKER_RULES));
|
|
239959
|
+
for (const body of topLevelDoBodies(rawStatement)) {
|
|
239960
|
+
const maskedBody = maskSqlNoise(body);
|
|
239961
|
+
const policyMaskedBody = maskSqlPolicyNoise(body);
|
|
239962
|
+
risks.push(...matchingRisks(body, maskedBody, DO_BODY_BLOCKER_RULES));
|
|
239963
|
+
risks.push(...matchingRisks(body, policyMaskedBody, QUOTED_FUNCTION_BLOCKER_RULES));
|
|
239964
|
+
risks.push(...matchingRisks(body, policyMaskedBody, MIGRATION_LEDGER_BLOCKER_RULES));
|
|
239965
|
+
}
|
|
239911
239966
|
}
|
|
239912
239967
|
return risks;
|
|
239913
239968
|
}
|
|
@@ -246635,9 +246690,9 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
246635
246690
|
className: cls.getName() ?? "<anonymous>",
|
|
246636
246691
|
name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
|
|
246637
246692
|
permission: stringLiteralProp(meta, "permission"),
|
|
246638
|
-
transaction:
|
|
246693
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
246639
246694
|
audit: stringLiteralProp(meta, "audit"),
|
|
246640
|
-
idempotency:
|
|
246695
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none"
|
|
246641
246696
|
});
|
|
246642
246697
|
}
|
|
246643
246698
|
}
|
|
@@ -246665,6 +246720,10 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
246665
246720
|
exports
|
|
246666
246721
|
};
|
|
246667
246722
|
}
|
|
246723
|
+
function commandModeProp(object, name) {
|
|
246724
|
+
const value = stringLiteralProp(object, name);
|
|
246725
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
246726
|
+
}
|
|
246668
246727
|
function parseProvider(el, exportsSet, ctx) {
|
|
246669
246728
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
|
|
246670
246729
|
const line = el.getStartLineNumber();
|
|
@@ -246817,6 +246876,11 @@ function parseController(el, ctx) {
|
|
|
246817
246876
|
schemaImports[schemaExpr.getText()] = importPath;
|
|
246818
246877
|
}
|
|
246819
246878
|
}
|
|
246879
|
+
const commandExpr = getProp(optionsArg, "command");
|
|
246880
|
+
if (commandExpr && import_ts_morph.Node.isIdentifier(commandExpr)) {
|
|
246881
|
+
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
246882
|
+
route.command = commandDecl && import_ts_morph.Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
|
|
246883
|
+
}
|
|
246820
246884
|
}
|
|
246821
246885
|
routes.push(route);
|
|
246822
246886
|
}
|
|
@@ -247052,6 +247116,16 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
247052
247116
|
params?: unknown;
|
|
247053
247117
|
query?: unknown;
|
|
247054
247118
|
response?: unknown;
|
|
247119
|
+
command?: string;
|
|
247120
|
+
}
|
|
247121
|
+
|
|
247122
|
+
export interface CompiledCommand {
|
|
247123
|
+
className: string;
|
|
247124
|
+
name: string;
|
|
247125
|
+
permission: string;
|
|
247126
|
+
transaction: "required" | "none";
|
|
247127
|
+
audit?: string;
|
|
247128
|
+
idempotency: "required" | "none";
|
|
247055
247129
|
}
|
|
247056
247130
|
|
|
247057
247131
|
export interface CompiledController {
|
|
@@ -247070,12 +247144,15 @@ export interface CompiledModule {
|
|
|
247070
247144
|
createRequestScope?(
|
|
247071
247145
|
services: Record<string, unknown>,
|
|
247072
247146
|
ctx: unknown,
|
|
247147
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
247073
247148
|
): Record<string, unknown>;
|
|
247074
247149
|
createJobScope?(
|
|
247075
247150
|
services: Record<string, unknown>,
|
|
247076
247151
|
ctx: unknown,
|
|
247152
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
247077
247153
|
): Record<string, unknown>;
|
|
247078
247154
|
controllers: CompiledController[];
|
|
247155
|
+
commands: CompiledCommand[];
|
|
247079
247156
|
}`;
|
|
247080
247157
|
async function generateApplication(graph, options) {
|
|
247081
247158
|
const modules = topoSortModules(graph.modules);
|
|
@@ -247094,7 +247171,7 @@ async function generateApplication(graph, options) {
|
|
|
247094
247171
|
...imports.size > 0 ? [""] : [],
|
|
247095
247172
|
INTERFACES,
|
|
247096
247173
|
"",
|
|
247097
|
-
"export function createCompiledModules(
|
|
247174
|
+
"export function createCompiledModules(): CompiledModule[] {",
|
|
247098
247175
|
" return [",
|
|
247099
247176
|
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
247100
247177
|
" ];",
|
|
@@ -247225,6 +247302,7 @@ class ModuleGenerator {
|
|
|
247225
247302
|
lines.push(` createJobScope: create${this.pascal}JobScope,`);
|
|
247226
247303
|
}
|
|
247227
247304
|
lines.push(` controllers: ${this.renderControllers()},`);
|
|
247305
|
+
lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
|
|
247228
247306
|
lines.push(`}`);
|
|
247229
247307
|
return lines.join(`
|
|
247230
247308
|
`);
|
|
@@ -247249,6 +247327,8 @@ class ModuleGenerator {
|
|
|
247249
247327
|
fields.push(`${field}: ${local}`);
|
|
247250
247328
|
}
|
|
247251
247329
|
}
|
|
247330
|
+
if (route.command)
|
|
247331
|
+
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
247252
247332
|
return `{ ${fields.join(", ")} }`;
|
|
247253
247333
|
});
|
|
247254
247334
|
return [
|
|
@@ -247282,6 +247362,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
247282
247362
|
`function create${this.pascal}${suffix}(`,
|
|
247283
247363
|
` services: Record<string, unknown>,`,
|
|
247284
247364
|
` ctx: unknown,`,
|
|
247365
|
+
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
247285
247366
|
`): Record<string, unknown> {`,
|
|
247286
247367
|
indent(this.renderFactoryBody(kind), 2),
|
|
247287
247368
|
`}`
|
|
@@ -247380,7 +247461,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
247380
247461
|
continue;
|
|
247381
247462
|
if (kind === "services")
|
|
247382
247463
|
return `imported.${importName}.${camelName(token)}`;
|
|
247383
|
-
return `
|
|
247464
|
+
return `imported.${importName}.${camelName(token)}`;
|
|
247384
247465
|
}
|
|
247385
247466
|
if (kind === "services")
|
|
247386
247467
|
return `deps.${camelName(token)}`;
|
|
@@ -247441,6 +247522,42 @@ function validateGraph(graph, strict = false) {
|
|
|
247441
247522
|
const warn2 = (code, message, file, line) => {
|
|
247442
247523
|
diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
|
|
247443
247524
|
};
|
|
247525
|
+
const modulesByName = new Map;
|
|
247526
|
+
const commandsByName = new Map;
|
|
247527
|
+
const routesByKey = new Map;
|
|
247528
|
+
for (const module of graph.modules) {
|
|
247529
|
+
const previousModule = modulesByName.get(module.name);
|
|
247530
|
+
if (previousModule) {
|
|
247531
|
+
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
247532
|
+
} else {
|
|
247533
|
+
modulesByName.set(module.name, module);
|
|
247534
|
+
}
|
|
247535
|
+
for (const command of module.commands) {
|
|
247536
|
+
const previousName = commandsByName.get(command.name);
|
|
247537
|
+
if (previousName) {
|
|
247538
|
+
error("duplicate-command", `command 名 ${command.name} 重复(首次由模块 ${previousName.module.name} 的 ${previousName.className} 声明)`, module.file, module.line);
|
|
247539
|
+
} else {
|
|
247540
|
+
commandsByName.set(command.name, { module, className: command.className });
|
|
247541
|
+
}
|
|
247542
|
+
}
|
|
247543
|
+
}
|
|
247544
|
+
for (const module of graph.modules) {
|
|
247545
|
+
for (const controller of module.controllers) {
|
|
247546
|
+
for (const route of controller.routes) {
|
|
247547
|
+
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
247548
|
+
const key = `${route.method} ${fullPath}`;
|
|
247549
|
+
const previous = routesByKey.get(key);
|
|
247550
|
+
if (previous) {
|
|
247551
|
+
error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
|
|
247552
|
+
} else {
|
|
247553
|
+
routesByKey.set(key, { module, controller });
|
|
247554
|
+
}
|
|
247555
|
+
if (route.command && !module.commands.some((command) => command.className === route.command)) {
|
|
247556
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
247557
|
+
}
|
|
247558
|
+
}
|
|
247559
|
+
}
|
|
247560
|
+
}
|
|
247444
247561
|
for (const module of graph.modules) {
|
|
247445
247562
|
const seen = new Map;
|
|
247446
247563
|
for (const provider of module.providers) {
|
|
@@ -247472,13 +247589,18 @@ function validateGraph(graph, strict = false) {
|
|
|
247472
247589
|
}
|
|
247473
247590
|
for (const command of module.commands) {
|
|
247474
247591
|
if (!command.permission) {
|
|
247475
|
-
|
|
247592
|
+
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
|
|
247476
247593
|
}
|
|
247477
247594
|
}
|
|
247478
247595
|
}
|
|
247479
247596
|
diagnostics.push(...detectCycles(graph, resolveDep));
|
|
247480
247597
|
return diagnostics;
|
|
247481
247598
|
}
|
|
247599
|
+
function joinRoutePaths(prefix, path) {
|
|
247600
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
247601
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
247602
|
+
return normalized.replace(/:[^/]+/g, ":param");
|
|
247603
|
+
}
|
|
247482
247604
|
function detectCycles(graph, resolveDep) {
|
|
247483
247605
|
const diagnostics = [];
|
|
247484
247606
|
const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
|
|
@@ -247921,6 +248043,18 @@ JOIN pg_language l ON l.oid = p.prolang
|
|
|
247921
248043
|
WHERE n.nspname = ANY($1)
|
|
247922
248044
|
ORDER BY n.nspname, p.proname
|
|
247923
248045
|
`;
|
|
248046
|
+
var TRIGGERS_SQL = `
|
|
248047
|
+
SELECT n.nspname AS schema,
|
|
248048
|
+
c.relname AS table,
|
|
248049
|
+
t.tgname AS name,
|
|
248050
|
+
t.tgenabled <> 'D' AS enabled
|
|
248051
|
+
FROM pg_trigger t
|
|
248052
|
+
JOIN pg_class c ON c.oid = t.tgrelid
|
|
248053
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
248054
|
+
WHERE NOT t.tgisinternal
|
|
248055
|
+
AND n.nspname = ANY($1)
|
|
248056
|
+
ORDER BY n.nspname, c.relname, t.tgname
|
|
248057
|
+
`;
|
|
247924
248058
|
var GRANTS_SQL = `
|
|
247925
248059
|
SELECT table_schema AS object_schema,
|
|
247926
248060
|
table_name AS object_name,
|
|
@@ -247953,10 +248087,11 @@ function extractSearchPath(config) {
|
|
|
247953
248087
|
}
|
|
247954
248088
|
async function readCatalog(executor, schemas = ["public"]) {
|
|
247955
248089
|
const params = [schemas];
|
|
247956
|
-
const [tableRows, policyRows, functionRows, grantRows] = await Promise.all([
|
|
248090
|
+
const [tableRows, policyRows, functionRows, triggerRows, grantRows] = await Promise.all([
|
|
247957
248091
|
executor.query(TABLES_SQL, params),
|
|
247958
248092
|
executor.query(POLICIES_SQL, params),
|
|
247959
248093
|
executor.query(FUNCTIONS_SQL, params),
|
|
248094
|
+
executor.query(TRIGGERS_SQL, params),
|
|
247960
248095
|
executor.query(GRANTS_SQL, params)
|
|
247961
248096
|
]);
|
|
247962
248097
|
return {
|
|
@@ -247982,6 +248117,12 @@ async function readCatalog(executor, schemas = ["public"]) {
|
|
|
247982
248117
|
searchPath: extractSearchPath(row.config),
|
|
247983
248118
|
language: row.language
|
|
247984
248119
|
})),
|
|
248120
|
+
triggers: triggerRows.map((row) => ({
|
|
248121
|
+
schema: row.schema,
|
|
248122
|
+
table: row.table,
|
|
248123
|
+
name: row.name,
|
|
248124
|
+
enabled: row.enabled
|
|
248125
|
+
})),
|
|
247985
248126
|
grants: grantRows.map((row) => ({
|
|
247986
248127
|
objectSchema: row.object_schema,
|
|
247987
248128
|
objectName: row.object_name,
|
|
@@ -248035,6 +248176,20 @@ function reconcileModule(module, catalog) {
|
|
|
248035
248176
|
push("error", "definer-without-search-path", fn.name, `security definer 函数 ${fn.name} 未设置固定 search_path(当前: ${cf.searchPath ?? "未设置"})`);
|
|
248036
248177
|
}
|
|
248037
248178
|
}
|
|
248179
|
+
for (const trigger of module.triggers) {
|
|
248180
|
+
const [schema, table] = splitQualifiedName(trigger.table);
|
|
248181
|
+
const found = catalog.triggers.some((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
|
|
248182
|
+
if (!found) {
|
|
248183
|
+
push("error", "missing-trigger", `${trigger.table}.${trigger.name}`, `声明的触发器 ${trigger.name} 在表 ${trigger.table} 的 catalog 中不存在`);
|
|
248184
|
+
}
|
|
248185
|
+
}
|
|
248186
|
+
const declaredTriggerKeys = new Set(module.triggers.map((t) => `${t.table}::${t.name}`));
|
|
248187
|
+
for (const ct of catalog.triggers) {
|
|
248188
|
+
const qualified = `${ct.schema}.${ct.table}`;
|
|
248189
|
+
if (ownedTables.has(qualified) && !declaredTriggerKeys.has(`${qualified}::${ct.name}`)) {
|
|
248190
|
+
push("warn", "undeclared-trigger", `${qualified}.${ct.name}`, `归属表 ${qualified} 上存在未声明的触发器 ${ct.name}${ct.enabled ? "" : "(已禁用)"},可能发生漂移`);
|
|
248191
|
+
}
|
|
248192
|
+
}
|
|
248038
248193
|
for (const table of module.tables) {
|
|
248039
248194
|
const [schema, name] = splitQualifiedName(table);
|
|
248040
248195
|
const ct = catalog.tables.find((t) => t.schema === schema && t.name === name);
|
|
@@ -248066,6 +248221,8 @@ var SET_SEARCH_PATH_RE = /\bset\s+search_path\b/i;
|
|
|
248066
248221
|
var GRANT_TO_PUBLIC_RE = /\bgrant\b[^;]*\bto\s+public\b/i;
|
|
248067
248222
|
var DROP_WITHOUT_IF_EXISTS_RE = /\bdrop\s+(?:table|column)\s+(?!if\s+exists\b)/i;
|
|
248068
248223
|
var ENABLE_RLS_RE = /\benable\s+row\s+level\s+security\b/i;
|
|
248224
|
+
var CREATE_POLICY_RE = /\bcreate\s+policy\b/i;
|
|
248225
|
+
var DROP_POLICY_IF_EXISTS_RE = /\bdrop\s+policy\s+if\s+exists\b/i;
|
|
248069
248226
|
function lineOf(sql, index) {
|
|
248070
248227
|
let line = 1;
|
|
248071
248228
|
for (let i = 0;i < index; i += 1) {
|
|
@@ -248106,6 +248263,19 @@ function lintSql(sql, file) {
|
|
|
248106
248263
|
line: lineOf(sql, drop.index)
|
|
248107
248264
|
});
|
|
248108
248265
|
}
|
|
248266
|
+
const createPolicy = CREATE_POLICY_RE.exec(sql);
|
|
248267
|
+
if (createPolicy) {
|
|
248268
|
+
const dropPolicy = DROP_POLICY_IF_EXISTS_RE.exec(sql);
|
|
248269
|
+
if (!dropPolicy || dropPolicy.index > createPolicy.index) {
|
|
248270
|
+
issues.push({
|
|
248271
|
+
severity: "warn",
|
|
248272
|
+
code: "non-idempotent-policy",
|
|
248273
|
+
message: "create policy 前缺少 drop policy if exists,策略不可重复执行",
|
|
248274
|
+
file,
|
|
248275
|
+
line: lineOf(sql, createPolicy.index)
|
|
248276
|
+
});
|
|
248277
|
+
}
|
|
248278
|
+
}
|
|
248109
248279
|
return issues;
|
|
248110
248280
|
}
|
|
248111
248281
|
async function lintModule(module, readFile2) {
|
|
@@ -250641,7 +250811,7 @@ function registerDeployTools(server2, http, options = {}) {
|
|
|
250641
250811
|
// package.json
|
|
250642
250812
|
var package_default = {
|
|
250643
250813
|
name: "@supacloud/cli",
|
|
250644
|
-
version: "0.
|
|
250814
|
+
version: "0.41.0",
|
|
250645
250815
|
description: "Project-scoped CLI for SupaCloud users",
|
|
250646
250816
|
type: "module",
|
|
250647
250817
|
main: "./dist/index.js",
|
|
@@ -250673,8 +250843,8 @@ var package_default = {
|
|
|
250673
250843
|
},
|
|
250674
250844
|
dependencies: {
|
|
250675
250845
|
"@sinclair/typebox": "^0.34.52",
|
|
250676
|
-
"@supacloud/compiler": "^0.
|
|
250677
|
-
"@supacloud/db": "^0.
|
|
250846
|
+
"@supacloud/compiler": "^0.2.0",
|
|
250847
|
+
"@supacloud/db": "^0.2.0",
|
|
250678
250848
|
fflate: "^0.8.3"
|
|
250679
250849
|
},
|
|
250680
250850
|
devDependencies: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@sinclair/typebox": "^0.34.52",
|
|
35
|
-
"@supacloud/compiler": "^0.
|
|
36
|
-
"@supacloud/db": "^0.
|
|
35
|
+
"@supacloud/compiler": "^0.2.0",
|
|
36
|
+
"@supacloud/db": "^0.2.0",
|
|
37
37
|
"fflate": "^0.8.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|