@supacloud/cli 0.21.2 → 0.22.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/README.md +7 -0
- package/dist/index.js +260 -149
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,6 +198,13 @@ it never empties a bucket. Mutation receipts bind `project_ref`, `bucket_id`,
|
|
|
198
198
|
name with another version, or a local version with another name, before either
|
|
199
199
|
dry-run reporting or apply can continue. This keeps the preview consistent with
|
|
200
200
|
the server conflict that would otherwise occur after deployment starts.
|
|
201
|
+
Mutation failures use the release-control receipt schema and never include the
|
|
202
|
+
server response body. `OUTCOME_UNKNOWN` requires a fresh migration inventory
|
|
203
|
+
read before deciding whether a retry is safe. A migration push failure also
|
|
204
|
+
reports the local files applied or skipped before the failed file, so operators
|
|
205
|
+
can reconcile a partially completed sequence without exposing SQL. Baseline
|
|
206
|
+
success requires a bounded migration-inventory readback that confirms every
|
|
207
|
+
requested version, name, baseline marker, and checksum.
|
|
201
208
|
|
|
202
209
|
`edge_functions deploy --path` bundles local TypeScript and dependencies with
|
|
203
210
|
Bun and runs a local syntax check before upload. The Management API validates and
|
package/dist/index.js
CHANGED
|
@@ -6897,6 +6897,38 @@ function projectRefPathSegment(ref, operation) {
|
|
|
6897
6897
|
return encodeURIComponent(ref);
|
|
6898
6898
|
}
|
|
6899
6899
|
|
|
6900
|
+
// src/shared/tools/release-control-response.ts
|
|
6901
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
6902
|
+
function releaseControlSuccess(operation, payload) {
|
|
6903
|
+
return releaseControlResponse({
|
|
6904
|
+
...payload,
|
|
6905
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
6906
|
+
ok: true,
|
|
6907
|
+
operation
|
|
6908
|
+
});
|
|
6909
|
+
}
|
|
6910
|
+
function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
|
|
6911
|
+
return releaseControlErrorResponse({
|
|
6912
|
+
...safeState,
|
|
6913
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
6914
|
+
ok: false,
|
|
6915
|
+
operation,
|
|
6916
|
+
error: { code, http_status: httpStatus }
|
|
6917
|
+
});
|
|
6918
|
+
}
|
|
6919
|
+
function releaseControlMutationFailure(operation, response, safeState = {}) {
|
|
6920
|
+
const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
|
|
6921
|
+
return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status, safeState) : releaseControlFailure(operation, "HTTP_ERROR", response.status, safeState);
|
|
6922
|
+
}
|
|
6923
|
+
function releaseControlResponse(payload) {
|
|
6924
|
+
return {
|
|
6925
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
6926
|
+
};
|
|
6927
|
+
}
|
|
6928
|
+
function releaseControlErrorResponse(payload) {
|
|
6929
|
+
return { ...releaseControlResponse(payload), isError: true };
|
|
6930
|
+
}
|
|
6931
|
+
|
|
6900
6932
|
// src/shared/tools/database-tools.ts
|
|
6901
6933
|
var MAX_MIGRATION_VERSION = 9223372036854775807n;
|
|
6902
6934
|
var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
|
|
@@ -7046,95 +7078,188 @@ function sortMigrationFiles(migrations) {
|
|
|
7046
7078
|
}
|
|
7047
7079
|
return sorted;
|
|
7048
7080
|
}
|
|
7049
|
-
function
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
for (const row of rows) {
|
|
7053
|
-
if (!row || typeof row !== "object")
|
|
7054
|
-
continue;
|
|
7055
|
-
const migration = row;
|
|
7056
|
-
if (migration.version != null)
|
|
7057
|
-
keys.add(String(migration.version));
|
|
7058
|
-
if (migration.name != null)
|
|
7059
|
-
keys.add(String(migration.name));
|
|
7060
|
-
}
|
|
7061
|
-
return keys;
|
|
7081
|
+
function normalizedMigrationStatement(statement) {
|
|
7082
|
+
return statement.replace(/\r\n?/g, `
|
|
7083
|
+
`).trim();
|
|
7062
7084
|
}
|
|
7063
7085
|
function migrationRows(data) {
|
|
7064
|
-
|
|
7065
|
-
|
|
7086
|
+
if (Array.isArray(data))
|
|
7087
|
+
return data;
|
|
7088
|
+
if (data && typeof data === "object" && Array.isArray(data.rows)) {
|
|
7089
|
+
return data.rows;
|
|
7090
|
+
}
|
|
7091
|
+
throw new Error("Invalid remote migration inventory");
|
|
7066
7092
|
}
|
|
7067
|
-
function
|
|
7068
|
-
|
|
7093
|
+
function migrationIdentity(row) {
|
|
7094
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
7095
|
+
throw new Error("Invalid remote migration identity");
|
|
7096
|
+
}
|
|
7097
|
+
const migration = row;
|
|
7098
|
+
if (!isMigrationInventoryVersion(migration.version) || !isMigrationInventoryName(migration.name)) {
|
|
7099
|
+
throw new Error("Invalid remote migration identity");
|
|
7100
|
+
}
|
|
7101
|
+
return { version: migration.version, name: migration.name, statements: migration.statements };
|
|
7069
7102
|
}
|
|
7070
7103
|
function migrationIdentities(data) {
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7104
|
+
const identities = migrationRows(data).map(migrationIdentity);
|
|
7105
|
+
const versions = new Set;
|
|
7106
|
+
const names = new Set;
|
|
7107
|
+
for (const identity of identities) {
|
|
7108
|
+
if (versions.has(identity.version) || identity.name !== null && names.has(identity.name)) {
|
|
7109
|
+
throw new Error("Invalid remote migration inventory");
|
|
7110
|
+
}
|
|
7111
|
+
versions.add(identity.version);
|
|
7112
|
+
if (identity.name !== null)
|
|
7113
|
+
names.add(identity.name);
|
|
7114
|
+
}
|
|
7115
|
+
return identities;
|
|
7075
7116
|
}
|
|
7076
|
-
function
|
|
7077
|
-
|
|
7078
|
-
const exactlyMatches = local.version === remote.version && local.name === remote.name;
|
|
7079
|
-
return reusesVersionOrName && !exactlyMatches && !isLegacyNameBoundMigration(local, remote, remoteMigrations);
|
|
7117
|
+
function singleRemoteStatement(remote) {
|
|
7118
|
+
return Array.isArray(remote.statements) && remote.statements.length === 1 && typeof remote.statements[0] === "string" ? remote.statements[0] : null;
|
|
7080
7119
|
}
|
|
7081
|
-
function
|
|
7120
|
+
function exactIdentityIsApplied(local, remote) {
|
|
7121
|
+
const statement = singleRemoteStatement(remote);
|
|
7122
|
+
if (statement === null)
|
|
7123
|
+
return false;
|
|
7124
|
+
if (statement === `baseline:${local.name}` || statement === `direct-apply:${local.name}`)
|
|
7125
|
+
return true;
|
|
7126
|
+
const rawChecksum = createHash("sha256").update(local.rawBytes).digest("hex");
|
|
7127
|
+
return statement === `sha256:${rawChecksum}` || statement.trim() === local.sql.trim();
|
|
7128
|
+
}
|
|
7129
|
+
function legacyIdentityIsApplied(local, remote) {
|
|
7130
|
+
const statement = singleRemoteStatement(remote);
|
|
7131
|
+
return remote.name === local.name && remote.version !== local.version && statement !== null && statement.trim() === local.sql.trim();
|
|
7132
|
+
}
|
|
7133
|
+
function migrationIdentityConflict(local) {
|
|
7134
|
+
return new Error(`Migration identity conflicts:
|
|
7135
|
+
- ${local.file} (${local.version}) conflicts with remote inventory`);
|
|
7136
|
+
}
|
|
7137
|
+
function migrationDisposition(local, remoteMigrations) {
|
|
7138
|
+
const sameVersion = remoteMigrations.filter((remote2) => remote2.version === local.version);
|
|
7139
|
+
const sameName = remoteMigrations.filter((remote2) => remote2.name === local.name);
|
|
7140
|
+
if (sameVersion.length > 1 || sameName.length > 1)
|
|
7141
|
+
throw migrationIdentityConflict(local);
|
|
7142
|
+
if (!sameVersion.length && !sameName.length)
|
|
7143
|
+
return "pending";
|
|
7144
|
+
if (sameVersion[0] && sameName[0] && sameVersion[0] !== sameName[0])
|
|
7145
|
+
throw migrationIdentityConflict(local);
|
|
7146
|
+
const remote = sameVersion[0] ?? sameName[0];
|
|
7147
|
+
const exactIdentity = remote.version === local.version && remote.name === local.name;
|
|
7148
|
+
if (exactIdentity && exactIdentityIsApplied(local, remote))
|
|
7149
|
+
return "applied";
|
|
7150
|
+
if (!sameVersion.length && legacyIdentityIsApplied(local, remote))
|
|
7151
|
+
return "applied";
|
|
7152
|
+
throw migrationIdentityConflict(local);
|
|
7153
|
+
}
|
|
7154
|
+
function migrationPushPlan(data, migrationFiles) {
|
|
7082
7155
|
const remoteMigrations = migrationIdentities(data);
|
|
7083
|
-
|
|
7156
|
+
const plan = { alreadyApplied: [], pending: [] };
|
|
7157
|
+
for (const migration of migrationFiles) {
|
|
7158
|
+
const disposition = migrationDisposition(migration, remoteMigrations);
|
|
7159
|
+
plan[disposition === "applied" ? "alreadyApplied" : "pending"].push(migration);
|
|
7160
|
+
}
|
|
7161
|
+
return plan;
|
|
7084
7162
|
}
|
|
7085
|
-
function
|
|
7086
|
-
|
|
7087
|
-
return new Set(migrationFiles.flatMap((localMigration) => remoteMigrations.some((remoteMigration) => isLegacyNameBoundMigration(localMigration, remoteMigration, remoteMigrations)) ? [migrationIdentityKey(localMigration.version, localMigration.name)] : []));
|
|
7163
|
+
function recordPayload(payload) {
|
|
7164
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
|
|
7088
7165
|
}
|
|
7089
|
-
function
|
|
7090
|
-
const
|
|
7091
|
-
if (!
|
|
7092
|
-
return;
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7166
|
+
function confirmedMigrationReceipt(payload, expected) {
|
|
7167
|
+
const receipt = recordPayload(payload);
|
|
7168
|
+
if (!receipt || !isMigrationInventoryVersion(receipt.version))
|
|
7169
|
+
return false;
|
|
7170
|
+
const responseStatements = [expected.sql.trim()];
|
|
7171
|
+
const checksumStatements = [normalizedMigrationStatement(expected.sql)];
|
|
7172
|
+
const name = expected.name.trim();
|
|
7173
|
+
if (receipt.name !== name || JSON.stringify(receipt.statements) !== JSON.stringify(responseStatements))
|
|
7174
|
+
return false;
|
|
7175
|
+
if (expected.version !== undefined && receipt.version !== expected.version)
|
|
7176
|
+
return false;
|
|
7177
|
+
return receipt.checksum === migrationInventoryChecksum({ version: receipt.version, name, statements: checksumStatements });
|
|
7098
7178
|
}
|
|
7099
|
-
function
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7179
|
+
function isAlreadyAppliedMigrationResponse(response, migration) {
|
|
7180
|
+
if (response.ok || response.status !== 409 || response.transportError || response.responseReadError)
|
|
7181
|
+
return false;
|
|
7182
|
+
if (!response.data || typeof response.data !== "object" || Array.isArray(response.data))
|
|
7183
|
+
return false;
|
|
7184
|
+
const body = response.data;
|
|
7185
|
+
const name = migration.name.trim();
|
|
7186
|
+
const statements = [normalizedMigrationStatement(migration.sql)];
|
|
7187
|
+
return body.code === "409" && body.message === "Migration already applied" && body.version === migration.version && body.name === name && body.checksum === migrationInventoryChecksum({ version: migration.version, name, statements });
|
|
7188
|
+
}
|
|
7189
|
+
function expectedBaselineChecksum(migration) {
|
|
7190
|
+
return migrationInventoryChecksum({
|
|
7191
|
+
version: migration.version,
|
|
7192
|
+
name: migration.name,
|
|
7193
|
+
statements: [`baseline:${migration.name}`]
|
|
7194
|
+
});
|
|
7113
7195
|
}
|
|
7114
|
-
function
|
|
7115
|
-
const
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7196
|
+
function confirmedBaselineReceipt(payload, migrations) {
|
|
7197
|
+
const receipt = recordPayload(payload);
|
|
7198
|
+
if (!receipt || typeof receipt.marked !== "number" || typeof receipt.already_applied !== "number")
|
|
7199
|
+
return null;
|
|
7200
|
+
if (!Number.isSafeInteger(receipt.marked) || !Number.isSafeInteger(receipt.already_applied))
|
|
7201
|
+
return null;
|
|
7202
|
+
if (receipt.marked < 0 || receipt.already_applied < 0)
|
|
7203
|
+
return null;
|
|
7204
|
+
if (receipt.marked + receipt.already_applied !== migrations.length)
|
|
7205
|
+
return null;
|
|
7206
|
+
if (!Array.isArray(receipt.migrations) || receipt.migrations.length !== receipt.marked)
|
|
7207
|
+
return null;
|
|
7208
|
+
const expected = new Map(migrations.map((migration) => [migration.version, migration]));
|
|
7209
|
+
const marked = [];
|
|
7210
|
+
for (const rawMigration of receipt.migrations) {
|
|
7211
|
+
const migration = recordPayload(rawMigration);
|
|
7212
|
+
if (!migration || typeof migration.version !== "string")
|
|
7213
|
+
return null;
|
|
7214
|
+
const local = expected.get(migration.version);
|
|
7215
|
+
if (!local || migration.name !== local.name || migration.checksum !== expectedBaselineChecksum(local))
|
|
7216
|
+
return null;
|
|
7217
|
+
expected.delete(migration.version);
|
|
7218
|
+
marked.push(local);
|
|
7130
7219
|
}
|
|
7131
|
-
return
|
|
7220
|
+
return expected.size === receipt.already_applied ? { marked, concurrentlyAlreadyApplied: receipt.already_applied } : null;
|
|
7132
7221
|
}
|
|
7133
|
-
function
|
|
7134
|
-
|
|
7222
|
+
function baselineInventoryIsApplied(payload, migrations) {
|
|
7223
|
+
const inventory = migrationInventory(payload);
|
|
7224
|
+
if (!inventory)
|
|
7135
7225
|
return false;
|
|
7136
|
-
const
|
|
7137
|
-
return
|
|
7226
|
+
const byVersion = new Map(inventory.map((migration) => [migration.version, migration]));
|
|
7227
|
+
return migrations.every((migration) => {
|
|
7228
|
+
const remote = byVersion.get(migration.version);
|
|
7229
|
+
return remote?.name === migration.name && remote.statements.length === 1 && remote.statements[0] === `baseline:${migration.name}` && remote.checksum === expectedBaselineChecksum(migration);
|
|
7230
|
+
});
|
|
7231
|
+
}
|
|
7232
|
+
function confirmedSqlBatchReceipt(payload, expectedCommands) {
|
|
7233
|
+
const receipt = recordPayload(payload);
|
|
7234
|
+
if (!receipt || receipt.command !== "BATCH" || !Array.isArray(receipt.statements))
|
|
7235
|
+
return false;
|
|
7236
|
+
if (receipt.statements.length !== expectedCommands.length)
|
|
7237
|
+
return false;
|
|
7238
|
+
return receipt.statements.every((rawStatement, index) => {
|
|
7239
|
+
const statement = recordPayload(rawStatement);
|
|
7240
|
+
if (!statement || statement.index !== index + 1 || statement.command !== expectedCommands[index])
|
|
7241
|
+
return false;
|
|
7242
|
+
if (typeof statement.rowCount !== "number" || !Number.isSafeInteger(statement.rowCount) || statement.rowCount < 0)
|
|
7243
|
+
return false;
|
|
7244
|
+
if (typeof statement.durationMs !== "number" || !Number.isFinite(statement.durationMs) || statement.durationMs < 0)
|
|
7245
|
+
return false;
|
|
7246
|
+
return true;
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7249
|
+
function rlsStatementCommands(policyMode) {
|
|
7250
|
+
return [
|
|
7251
|
+
"CREATE",
|
|
7252
|
+
"ALTER",
|
|
7253
|
+
...Array.from({ length: 5 }, () => "DROP"),
|
|
7254
|
+
...policyMode === "owner" ? Array.from({ length: 4 }, () => "CREATE") : []
|
|
7255
|
+
];
|
|
7256
|
+
}
|
|
7257
|
+
function migrationPushFailureState(applied, skipped, failedFile) {
|
|
7258
|
+
return {
|
|
7259
|
+
applied_before_failure: [...applied],
|
|
7260
|
+
skipped_before_failure: [...skipped],
|
|
7261
|
+
failed_file: failedFile
|
|
7262
|
+
};
|
|
7138
7263
|
}
|
|
7139
7264
|
function sqlReferencesVector(sql) {
|
|
7140
7265
|
return /\bvector\s*\(\s*\d+\s*\)/i.test(sql) || /::\s*vector\b/i.test(sql) || /\bvector_(cosine|l2|ip)_ops\b/i.test(sql) || /<=>|<#>|<->/.test(sql);
|
|
@@ -7339,8 +7464,16 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7339
7464
|
case "apply_migration": {
|
|
7340
7465
|
if (!args.name || !args.sql)
|
|
7341
7466
|
throw new Error("'name' and 'sql' required");
|
|
7342
|
-
const r = await http.
|
|
7343
|
-
|
|
7467
|
+
const r = await http.postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name: args.name, sql: args.sql });
|
|
7468
|
+
if (!r.ok)
|
|
7469
|
+
return releaseControlMutationFailure("database.apply_migration", r);
|
|
7470
|
+
if (r.status !== 200) {
|
|
7471
|
+
return releaseControlFailure("database.apply_migration", "OUTCOME_UNKNOWN", r.status);
|
|
7472
|
+
}
|
|
7473
|
+
if (!confirmedMigrationReceipt(r.data, { name: args.name, sql: args.sql })) {
|
|
7474
|
+
return releaseControlFailure("database.apply_migration", "OUTCOME_UNKNOWN", r.status);
|
|
7475
|
+
}
|
|
7476
|
+
text = `✅ Migration '${args.name}' applied`;
|
|
7344
7477
|
break;
|
|
7345
7478
|
}
|
|
7346
7479
|
case "push_migrations": {
|
|
@@ -7356,15 +7489,13 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7356
7489
|
const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
7357
7490
|
const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
|
|
7358
7491
|
if (!migrationsResult.ok) {
|
|
7359
|
-
|
|
7360
|
-
break;
|
|
7492
|
+
return releaseControlFailure("database.push_migrations", "HTTP_ERROR", migrationsResult.transportError ? null : migrationsResult.status);
|
|
7361
7493
|
}
|
|
7362
|
-
|
|
7494
|
+
const migrationPlan = migrationPushPlan(migrationsResult.data, migrationFiles);
|
|
7363
7495
|
if (args.dry_run) {
|
|
7364
|
-
const
|
|
7365
|
-
const
|
|
7366
|
-
const
|
|
7367
|
-
const pendingWithSql = pending;
|
|
7496
|
+
const pending2 = migrationPlan.pending;
|
|
7497
|
+
const alreadyApplied = migrationPlan.alreadyApplied;
|
|
7498
|
+
const pendingWithSql = pending2;
|
|
7368
7499
|
let vectorEnabled = null;
|
|
7369
7500
|
if (pendingWithSql.some(({ sql }) => sqlReferencesVector(sql) || sqlCreatesVectorExtension(sql))) {
|
|
7370
7501
|
const extResult = await execSql("SELECT extname AS name FROM pg_extension WHERE extname = 'vector';");
|
|
@@ -7376,7 +7507,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7376
7507
|
`Total: ${migrationFiles.length}`,
|
|
7377
7508
|
"",
|
|
7378
7509
|
"Pending:",
|
|
7379
|
-
...
|
|
7510
|
+
...pending2.length ? pending2.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
|
|
7380
7511
|
"",
|
|
7381
7512
|
"Already applied:",
|
|
7382
7513
|
...alreadyApplied.length ? alreadyApplied.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
|
|
@@ -7385,32 +7516,25 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7385
7516
|
`);
|
|
7386
7517
|
break;
|
|
7387
7518
|
}
|
|
7519
|
+
const pending = new Set(migrationPlan.pending.map(({ file }) => file));
|
|
7388
7520
|
const applied = [];
|
|
7389
7521
|
const skipped = [];
|
|
7390
|
-
const nameBoundMarkerKeys = nameBoundMigrationMarkerKeys(migrationsResult.data);
|
|
7391
|
-
const historicalChecksumKeys = historicalChecksumMarkerKeys(migrationsResult.data, migrationFiles);
|
|
7392
|
-
const legacyNameBoundKeys = legacyNameBoundMigrationKeys(migrationsResult.data, migrationFiles);
|
|
7393
7522
|
for (const { file, name, version, sql } of migrationFiles) {
|
|
7394
|
-
|
|
7395
|
-
if (nameBoundMarkerKeys.has(migrationKey) || historicalChecksumKeys.has(migrationKey) || legacyNameBoundKeys.has(migrationKey)) {
|
|
7523
|
+
if (!pending.has(file)) {
|
|
7396
7524
|
skipped.push(file);
|
|
7397
7525
|
continue;
|
|
7398
7526
|
}
|
|
7399
|
-
const r = await http.
|
|
7400
|
-
if (r.ok) {
|
|
7527
|
+
const r = await http.postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
|
|
7528
|
+
if (r.ok && r.status === 200 && confirmedMigrationReceipt(r.data, { name, version, sql })) {
|
|
7401
7529
|
applied.push(file);
|
|
7402
|
-
} else if (isAlreadyAppliedMigrationResponse(r)) {
|
|
7530
|
+
} else if (isAlreadyAppliedMigrationResponse(r, { name, version, sql })) {
|
|
7403
7531
|
skipped.push(file);
|
|
7404
7532
|
} else {
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
`Skipped before failure: ${skipped.length}`
|
|
7411
|
-
].join(`
|
|
7412
|
-
`);
|
|
7413
|
-
return { content: [{ type: "text", text }] };
|
|
7533
|
+
const safeState = migrationPushFailureState(applied, skipped, file);
|
|
7534
|
+
if (r.ok) {
|
|
7535
|
+
return releaseControlFailure("database.push_migrations", "OUTCOME_UNKNOWN", r.status, safeState);
|
|
7536
|
+
}
|
|
7537
|
+
return releaseControlMutationFailure("database.push_migrations", r, safeState);
|
|
7414
7538
|
}
|
|
7415
7539
|
}
|
|
7416
7540
|
text = [
|
|
@@ -7436,12 +7560,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7436
7560
|
const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
7437
7561
|
const r = await http.get(`/v1/projects/${ref}/database/migrations`);
|
|
7438
7562
|
if (!r.ok) {
|
|
7439
|
-
|
|
7440
|
-
break;
|
|
7563
|
+
return releaseControlFailure("database.baseline_migrations", "HTTP_ERROR", r.transportError ? null : r.status);
|
|
7441
7564
|
}
|
|
7442
|
-
const
|
|
7443
|
-
const missing =
|
|
7444
|
-
const alreadyApplied =
|
|
7565
|
+
const migrationPlan = migrationPushPlan(r.data, migrationFiles);
|
|
7566
|
+
const missing = migrationPlan.pending;
|
|
7567
|
+
const alreadyApplied = migrationPlan.alreadyApplied;
|
|
7445
7568
|
if (args.dry_run) {
|
|
7446
7569
|
text = [
|
|
7447
7570
|
`Migration baseline dry run for ${dir}`,
|
|
@@ -7464,16 +7587,28 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7464
7587
|
`);
|
|
7465
7588
|
break;
|
|
7466
7589
|
}
|
|
7467
|
-
const baselineResult = await http.
|
|
7468
|
-
|
|
7590
|
+
const baselineResult = await http.postReleaseMutation(`/v1/projects/${ref}/database/migrations/baseline`, { migrations: missing.map(({ name, version }) => ({ name, version })) });
|
|
7591
|
+
if (!baselineResult.ok) {
|
|
7592
|
+
return releaseControlMutationFailure("database.baseline_migrations", baselineResult);
|
|
7593
|
+
}
|
|
7594
|
+
if (baselineResult.status !== 200) {
|
|
7595
|
+
return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", baselineResult.status);
|
|
7596
|
+
}
|
|
7597
|
+
const receipt = confirmedBaselineReceipt(baselineResult.data, missing);
|
|
7598
|
+
if (!receipt) {
|
|
7599
|
+
return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", baselineResult.status);
|
|
7600
|
+
}
|
|
7601
|
+
const inventoryResult = await http.get(`/v1/projects/${ref}/database/migrations`, { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
|
|
7602
|
+
if (!inventoryResult.ok || !baselineInventoryIsApplied(inventoryResult.data, missing)) {
|
|
7603
|
+
return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", inventoryResult.transportError ? null : inventoryResult.status);
|
|
7604
|
+
}
|
|
7605
|
+
text = [
|
|
7469
7606
|
`✅ Migration baseline completed for ${dir}`,
|
|
7470
|
-
`Marked applied: ${
|
|
7471
|
-
`Already applied: ${alreadyApplied.length}`,
|
|
7472
|
-
"",
|
|
7473
|
-
"Marked files:",
|
|
7474
|
-
...missing.map(({ file, version }) => ` - ${file} (${version})`)
|
|
7607
|
+
`Marked applied: ${receipt.marked.length}`,
|
|
7608
|
+
`Already applied: ${alreadyApplied.length + receipt.concurrentlyAlreadyApplied}`,
|
|
7609
|
+
...receipt.marked.length ? ["", "Marked files:", ...receipt.marked.map(({ file, version }) => ` - ${file} (${version})`)] : []
|
|
7475
7610
|
].join(`
|
|
7476
|
-
`)
|
|
7611
|
+
`);
|
|
7477
7612
|
break;
|
|
7478
7613
|
}
|
|
7479
7614
|
case "create_table_rls": {
|
|
@@ -7486,8 +7621,16 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7486
7621
|
throw new Error("Invalid RLS policy mode");
|
|
7487
7622
|
const policySql = buildRlsPolicySql(qualifiedTable, policyMode, args.owner_column);
|
|
7488
7623
|
const sql = `BEGIN; CREATE TABLE IF NOT EXISTS ${qualifiedTable} (${columns}); ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY; ${policySql} COMMIT;`;
|
|
7489
|
-
const r = await
|
|
7490
|
-
|
|
7624
|
+
const r = await http.postReleaseMutation(`/v1/projects/${ref}/database/sql`, { sql, mode: "migration" });
|
|
7625
|
+
if (!r.ok)
|
|
7626
|
+
return releaseControlMutationFailure("database.create_table_rls", r);
|
|
7627
|
+
if (r.status !== 200) {
|
|
7628
|
+
return releaseControlFailure("database.create_table_rls", "OUTCOME_UNKNOWN", r.status);
|
|
7629
|
+
}
|
|
7630
|
+
if (!confirmedSqlBatchReceipt(r.data, rlsStatementCommands(policyMode))) {
|
|
7631
|
+
return releaseControlFailure("database.create_table_rls", "OUTCOME_UNKNOWN", r.status);
|
|
7632
|
+
}
|
|
7633
|
+
text = `✅ Table '${schema}.${args.table}' created with RLS (${policyMode === "owner" ? "auth.uid() owner policy" : "deny-all by default"})`;
|
|
7491
7634
|
break;
|
|
7492
7635
|
}
|
|
7493
7636
|
default:
|
|
@@ -7978,38 +8121,6 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7978
8121
|
});
|
|
7979
8122
|
}
|
|
7980
8123
|
|
|
7981
|
-
// src/shared/tools/release-control-response.ts
|
|
7982
|
-
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
7983
|
-
function releaseControlSuccess(operation, payload) {
|
|
7984
|
-
return releaseControlResponse({
|
|
7985
|
-
...payload,
|
|
7986
|
-
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7987
|
-
ok: true,
|
|
7988
|
-
operation
|
|
7989
|
-
});
|
|
7990
|
-
}
|
|
7991
|
-
function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
|
|
7992
|
-
return releaseControlErrorResponse({
|
|
7993
|
-
...safeState,
|
|
7994
|
-
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7995
|
-
ok: false,
|
|
7996
|
-
operation,
|
|
7997
|
-
error: { code, http_status: httpStatus }
|
|
7998
|
-
});
|
|
7999
|
-
}
|
|
8000
|
-
function releaseControlMutationFailure(operation, response) {
|
|
8001
|
-
const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
|
|
8002
|
-
return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
8003
|
-
}
|
|
8004
|
-
function releaseControlResponse(payload) {
|
|
8005
|
-
return {
|
|
8006
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
8007
|
-
};
|
|
8008
|
-
}
|
|
8009
|
-
function releaseControlErrorResponse(payload) {
|
|
8010
|
-
return { ...releaseControlResponse(payload), isError: true };
|
|
8011
|
-
}
|
|
8012
|
-
|
|
8013
8124
|
// src/shared/tools/storage-tools.ts
|
|
8014
8125
|
var MAX_BUCKET_ID_LENGTH = 100;
|
|
8015
8126
|
var MAX_MIME_TYPE_COUNT = 100;
|
|
@@ -12047,7 +12158,7 @@ var MUTATION_TOOL_SCHEMA = {
|
|
|
12047
12158
|
// package.json
|
|
12048
12159
|
var package_default = {
|
|
12049
12160
|
name: "@supacloud/cli",
|
|
12050
|
-
version: "0.
|
|
12161
|
+
version: "0.22.0",
|
|
12051
12162
|
description: "Project-scoped CLI for SupaCloud users",
|
|
12052
12163
|
type: "module",
|
|
12053
12164
|
main: "./dist/index.js",
|