smoonb 1.0.7 → 1.0.8
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/CHANGELOG.md +26 -0
- package/README.md +23 -7
- package/README.pt-BR.md +23 -7
- package/package.json +4 -3
- package/src/commands/backup/steps/06-realtime-settings.js +19 -33
- package/src/commands/backup/steps/07-storage.js +7 -12
- package/src/commands/backup/steps/09-edge-functions.js +76 -202
- package/src/commands/restore/auth-fields.json +237 -0
- package/src/commands/restore/dump.js +284 -0
- package/src/commands/restore/index.js +4 -126
- package/src/commands/restore/management.js +74 -0
- package/src/commands/restore/pg.js +83 -0
- package/src/commands/restore/runner.js +117 -0
- package/src/commands/restore/steps/03-database.js +18 -84
- package/src/commands/restore/steps/04-edge-functions.js +95 -122
- package/src/commands/restore/steps/05-auth-settings.js +26 -54
- package/src/commands/restore/steps/06-storage.js +203 -584
- package/src/commands/restore/steps/07-database-settings.js +87 -69
- package/src/commands/restore/steps/08-realtime-settings.js +61 -53
- package/src/i18n/locales/en.json +797 -797
- package/src/i18n/locales/pt-BR.json +796 -796
- package/src/utils/supabaseCli.js +32 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
[
|
|
2
|
+
"site_url",
|
|
3
|
+
"disable_signup",
|
|
4
|
+
"jwt_exp",
|
|
5
|
+
"smtp_admin_email",
|
|
6
|
+
"smtp_host",
|
|
7
|
+
"smtp_port",
|
|
8
|
+
"smtp_user",
|
|
9
|
+
"smtp_pass",
|
|
10
|
+
"smtp_max_frequency",
|
|
11
|
+
"smtp_sender_name",
|
|
12
|
+
"mailer_allow_unverified_email_sign_ins",
|
|
13
|
+
"mailer_autoconfirm",
|
|
14
|
+
"mailer_subjects_invite",
|
|
15
|
+
"mailer_subjects_confirmation",
|
|
16
|
+
"mailer_subjects_recovery",
|
|
17
|
+
"mailer_subjects_email_change",
|
|
18
|
+
"mailer_subjects_magic_link",
|
|
19
|
+
"mailer_subjects_reauthentication",
|
|
20
|
+
"mailer_subjects_password_changed_notification",
|
|
21
|
+
"mailer_subjects_email_changed_notification",
|
|
22
|
+
"mailer_subjects_phone_changed_notification",
|
|
23
|
+
"mailer_subjects_mfa_factor_enrolled_notification",
|
|
24
|
+
"mailer_subjects_mfa_factor_unenrolled_notification",
|
|
25
|
+
"mailer_subjects_identity_linked_notification",
|
|
26
|
+
"mailer_subjects_identity_unlinked_notification",
|
|
27
|
+
"mailer_templates_invite_content",
|
|
28
|
+
"mailer_templates_confirmation_content",
|
|
29
|
+
"mailer_templates_recovery_content",
|
|
30
|
+
"mailer_templates_email_change_content",
|
|
31
|
+
"mailer_templates_magic_link_content",
|
|
32
|
+
"mailer_templates_reauthentication_content",
|
|
33
|
+
"mailer_templates_password_changed_notification_content",
|
|
34
|
+
"mailer_templates_email_changed_notification_content",
|
|
35
|
+
"mailer_templates_phone_changed_notification_content",
|
|
36
|
+
"mailer_templates_mfa_factor_enrolled_notification_content",
|
|
37
|
+
"mailer_templates_mfa_factor_unenrolled_notification_content",
|
|
38
|
+
"mailer_templates_identity_linked_notification_content",
|
|
39
|
+
"mailer_templates_identity_unlinked_notification_content",
|
|
40
|
+
"mailer_notifications_password_changed_enabled",
|
|
41
|
+
"mailer_notifications_email_changed_enabled",
|
|
42
|
+
"mailer_notifications_phone_changed_enabled",
|
|
43
|
+
"mailer_notifications_mfa_factor_enrolled_enabled",
|
|
44
|
+
"mailer_notifications_mfa_factor_unenrolled_enabled",
|
|
45
|
+
"mailer_notifications_identity_linked_enabled",
|
|
46
|
+
"mailer_notifications_identity_unlinked_enabled",
|
|
47
|
+
"mfa_max_enrolled_factors",
|
|
48
|
+
"uri_allow_list",
|
|
49
|
+
"external_anonymous_users_enabled",
|
|
50
|
+
"external_email_enabled",
|
|
51
|
+
"external_phone_enabled",
|
|
52
|
+
"saml_enabled",
|
|
53
|
+
"saml_external_url",
|
|
54
|
+
"security_sb_forwarded_for_enabled",
|
|
55
|
+
"security_captcha_enabled",
|
|
56
|
+
"security_captcha_provider",
|
|
57
|
+
"security_captcha_secret",
|
|
58
|
+
"sessions_timebox",
|
|
59
|
+
"sessions_inactivity_timeout",
|
|
60
|
+
"sessions_single_per_user",
|
|
61
|
+
"sessions_tags",
|
|
62
|
+
"rate_limit_anonymous_users",
|
|
63
|
+
"rate_limit_email_sent",
|
|
64
|
+
"rate_limit_sms_sent",
|
|
65
|
+
"rate_limit_verify",
|
|
66
|
+
"rate_limit_token_refresh",
|
|
67
|
+
"rate_limit_otp",
|
|
68
|
+
"rate_limit_web3",
|
|
69
|
+
"mailer_secure_email_change_enabled",
|
|
70
|
+
"refresh_token_rotation_enabled",
|
|
71
|
+
"password_hibp_enabled",
|
|
72
|
+
"password_min_length",
|
|
73
|
+
"password_required_characters",
|
|
74
|
+
"security_manual_linking_enabled",
|
|
75
|
+
"security_update_password_require_reauthentication",
|
|
76
|
+
"security_update_password_require_current_password",
|
|
77
|
+
"security_refresh_token_reuse_interval",
|
|
78
|
+
"mailer_otp_exp",
|
|
79
|
+
"mailer_otp_length",
|
|
80
|
+
"sms_autoconfirm",
|
|
81
|
+
"sms_max_frequency",
|
|
82
|
+
"sms_otp_exp",
|
|
83
|
+
"sms_otp_length",
|
|
84
|
+
"sms_provider",
|
|
85
|
+
"sms_messagebird_access_key",
|
|
86
|
+
"sms_messagebird_originator",
|
|
87
|
+
"sms_test_otp",
|
|
88
|
+
"sms_test_otp_valid_until",
|
|
89
|
+
"sms_textlocal_api_key",
|
|
90
|
+
"sms_textlocal_sender",
|
|
91
|
+
"sms_twilio_account_sid",
|
|
92
|
+
"sms_twilio_auth_token",
|
|
93
|
+
"sms_twilio_content_sid",
|
|
94
|
+
"sms_twilio_message_service_sid",
|
|
95
|
+
"sms_twilio_verify_account_sid",
|
|
96
|
+
"sms_twilio_verify_auth_token",
|
|
97
|
+
"sms_twilio_verify_message_service_sid",
|
|
98
|
+
"sms_vonage_api_key",
|
|
99
|
+
"sms_vonage_api_secret",
|
|
100
|
+
"sms_vonage_from",
|
|
101
|
+
"sms_template",
|
|
102
|
+
"hook_mfa_verification_attempt_enabled",
|
|
103
|
+
"hook_mfa_verification_attempt_uri",
|
|
104
|
+
"hook_mfa_verification_attempt_secrets",
|
|
105
|
+
"hook_password_verification_attempt_enabled",
|
|
106
|
+
"hook_password_verification_attempt_uri",
|
|
107
|
+
"hook_password_verification_attempt_secrets",
|
|
108
|
+
"hook_custom_access_token_enabled",
|
|
109
|
+
"hook_custom_access_token_uri",
|
|
110
|
+
"hook_custom_access_token_secrets",
|
|
111
|
+
"hook_send_sms_enabled",
|
|
112
|
+
"hook_send_sms_uri",
|
|
113
|
+
"hook_send_sms_secrets",
|
|
114
|
+
"hook_send_email_enabled",
|
|
115
|
+
"hook_send_email_uri",
|
|
116
|
+
"hook_send_email_secrets",
|
|
117
|
+
"hook_before_user_created_enabled",
|
|
118
|
+
"hook_before_user_created_uri",
|
|
119
|
+
"hook_before_user_created_secrets",
|
|
120
|
+
"hook_after_user_created_enabled",
|
|
121
|
+
"hook_after_user_created_uri",
|
|
122
|
+
"hook_after_user_created_secrets",
|
|
123
|
+
"external_apple_enabled",
|
|
124
|
+
"external_apple_client_id",
|
|
125
|
+
"external_apple_email_optional",
|
|
126
|
+
"external_apple_secret",
|
|
127
|
+
"external_apple_additional_client_ids",
|
|
128
|
+
"external_azure_enabled",
|
|
129
|
+
"external_azure_client_id",
|
|
130
|
+
"external_azure_email_optional",
|
|
131
|
+
"external_azure_secret",
|
|
132
|
+
"external_azure_url",
|
|
133
|
+
"external_bitbucket_enabled",
|
|
134
|
+
"external_bitbucket_client_id",
|
|
135
|
+
"external_bitbucket_email_optional",
|
|
136
|
+
"external_bitbucket_secret",
|
|
137
|
+
"external_discord_enabled",
|
|
138
|
+
"external_discord_client_id",
|
|
139
|
+
"external_discord_email_optional",
|
|
140
|
+
"external_discord_secret",
|
|
141
|
+
"external_facebook_enabled",
|
|
142
|
+
"external_facebook_client_id",
|
|
143
|
+
"external_facebook_email_optional",
|
|
144
|
+
"external_facebook_secret",
|
|
145
|
+
"external_figma_enabled",
|
|
146
|
+
"external_figma_client_id",
|
|
147
|
+
"external_figma_email_optional",
|
|
148
|
+
"external_figma_secret",
|
|
149
|
+
"external_github_enabled",
|
|
150
|
+
"external_github_client_id",
|
|
151
|
+
"external_github_email_optional",
|
|
152
|
+
"external_github_secret",
|
|
153
|
+
"external_gitlab_enabled",
|
|
154
|
+
"external_gitlab_client_id",
|
|
155
|
+
"external_gitlab_email_optional",
|
|
156
|
+
"external_gitlab_secret",
|
|
157
|
+
"external_gitlab_url",
|
|
158
|
+
"external_google_enabled",
|
|
159
|
+
"external_google_client_id",
|
|
160
|
+
"external_google_email_optional",
|
|
161
|
+
"external_google_secret",
|
|
162
|
+
"external_google_additional_client_ids",
|
|
163
|
+
"external_google_skip_nonce_check",
|
|
164
|
+
"external_kakao_enabled",
|
|
165
|
+
"external_kakao_client_id",
|
|
166
|
+
"external_kakao_email_optional",
|
|
167
|
+
"external_kakao_secret",
|
|
168
|
+
"external_keycloak_enabled",
|
|
169
|
+
"external_keycloak_client_id",
|
|
170
|
+
"external_keycloak_email_optional",
|
|
171
|
+
"external_keycloak_secret",
|
|
172
|
+
"external_keycloak_url",
|
|
173
|
+
"external_linkedin_oidc_enabled",
|
|
174
|
+
"external_linkedin_oidc_client_id",
|
|
175
|
+
"external_linkedin_oidc_email_optional",
|
|
176
|
+
"external_linkedin_oidc_secret",
|
|
177
|
+
"external_slack_oidc_enabled",
|
|
178
|
+
"external_slack_oidc_client_id",
|
|
179
|
+
"external_slack_oidc_email_optional",
|
|
180
|
+
"external_slack_oidc_secret",
|
|
181
|
+
"external_notion_enabled",
|
|
182
|
+
"external_notion_client_id",
|
|
183
|
+
"external_notion_email_optional",
|
|
184
|
+
"external_notion_secret",
|
|
185
|
+
"external_slack_enabled",
|
|
186
|
+
"external_slack_client_id",
|
|
187
|
+
"external_slack_email_optional",
|
|
188
|
+
"external_slack_secret",
|
|
189
|
+
"external_spotify_enabled",
|
|
190
|
+
"external_spotify_client_id",
|
|
191
|
+
"external_spotify_email_optional",
|
|
192
|
+
"external_spotify_secret",
|
|
193
|
+
"external_twitch_enabled",
|
|
194
|
+
"external_twitch_client_id",
|
|
195
|
+
"external_twitch_email_optional",
|
|
196
|
+
"external_twitch_secret",
|
|
197
|
+
"external_twitter_enabled",
|
|
198
|
+
"external_twitter_client_id",
|
|
199
|
+
"external_twitter_email_optional",
|
|
200
|
+
"external_twitter_secret",
|
|
201
|
+
"external_x_enabled",
|
|
202
|
+
"external_x_client_id",
|
|
203
|
+
"external_x_email_optional",
|
|
204
|
+
"external_x_secret",
|
|
205
|
+
"external_workos_enabled",
|
|
206
|
+
"external_workos_client_id",
|
|
207
|
+
"external_workos_secret",
|
|
208
|
+
"external_workos_url",
|
|
209
|
+
"external_web3_solana_enabled",
|
|
210
|
+
"external_web3_ethereum_enabled",
|
|
211
|
+
"external_zoom_enabled",
|
|
212
|
+
"external_zoom_client_id",
|
|
213
|
+
"external_zoom_email_optional",
|
|
214
|
+
"external_zoom_secret",
|
|
215
|
+
"db_max_pool_size",
|
|
216
|
+
"db_max_pool_size_unit",
|
|
217
|
+
"api_max_request_duration",
|
|
218
|
+
"mfa_totp_enroll_enabled",
|
|
219
|
+
"mfa_totp_verify_enabled",
|
|
220
|
+
"mfa_web_authn_enroll_enabled",
|
|
221
|
+
"mfa_web_authn_verify_enabled",
|
|
222
|
+
"passkey_enabled",
|
|
223
|
+
"webauthn_rp_display_name",
|
|
224
|
+
"webauthn_rp_id",
|
|
225
|
+
"webauthn_rp_origins",
|
|
226
|
+
"mfa_phone_enroll_enabled",
|
|
227
|
+
"mfa_phone_verify_enabled",
|
|
228
|
+
"mfa_phone_max_frequency",
|
|
229
|
+
"mfa_phone_otp_length",
|
|
230
|
+
"mfa_phone_template",
|
|
231
|
+
"nimbus_oauth_client_id",
|
|
232
|
+
"nimbus_oauth_client_secret",
|
|
233
|
+
"oauth_server_enabled",
|
|
234
|
+
"oauth_server_allow_dynamic_registration",
|
|
235
|
+
"oauth_server_authorization_path",
|
|
236
|
+
"custom_oauth_enabled"
|
|
237
|
+
]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const zlib = require('zlib');
|
|
3
|
+
const { ident, literal } = require('./pg');
|
|
4
|
+
const managed = new Set([
|
|
5
|
+
'auth',
|
|
6
|
+
'storage',
|
|
7
|
+
'realtime',
|
|
8
|
+
'_realtime',
|
|
9
|
+
'extensions',
|
|
10
|
+
'graphql',
|
|
11
|
+
'graphql_public',
|
|
12
|
+
'pgbouncer',
|
|
13
|
+
'vault',
|
|
14
|
+
'supabase_functions',
|
|
15
|
+
'net',
|
|
16
|
+
'cron',
|
|
17
|
+
'pgsodium',
|
|
18
|
+
'pgsodium_masks',
|
|
19
|
+
]);
|
|
20
|
+
const isApplication = (schema) =>
|
|
21
|
+
schema !== '-' && !managed.has(schema) && !schema.startsWith('pg_');
|
|
22
|
+
|
|
23
|
+
function readDump(file) {
|
|
24
|
+
const bytes = fs.readFileSync(file);
|
|
25
|
+
const text = (file.endsWith('.gz') ? zlib.gunzipSync(bytes) : bytes)
|
|
26
|
+
.toString('utf8')
|
|
27
|
+
.replace(/\r\n/g, '\n');
|
|
28
|
+
const matches = [
|
|
29
|
+
...text.matchAll(
|
|
30
|
+
/^-- (?:Data for )?Name: (.*); Type: ([A-Z ]+); Schema: (.*); Owner: (.*)$/gm,
|
|
31
|
+
),
|
|
32
|
+
];
|
|
33
|
+
if (!matches.length)
|
|
34
|
+
throw new Error('Unsupported or incomplete plain PostgreSQL backup');
|
|
35
|
+
const sections = matches.map((m, i) => ({
|
|
36
|
+
name: m[1],
|
|
37
|
+
type: m[2],
|
|
38
|
+
schema: m[3],
|
|
39
|
+
owner: m[4],
|
|
40
|
+
sql: text
|
|
41
|
+
.slice(m.index + m[0].length, matches[i + 1]?.index ?? text.length)
|
|
42
|
+
.replace(/^(?:\n|--\n)+/, '')
|
|
43
|
+
.trim(),
|
|
44
|
+
}));
|
|
45
|
+
return {
|
|
46
|
+
sections,
|
|
47
|
+
tables: sections.filter((s) => s.type === 'TABLE DATA').map(parseCopy),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseCopy(section) {
|
|
51
|
+
const m = section.sql.match(
|
|
52
|
+
/^COPY (.+?) \((.+)\) FROM stdin;\n([\s\S]*?)^\\\.$/m,
|
|
53
|
+
);
|
|
54
|
+
if (!m)
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Unsupported data section: ${section.schema}.${section.name}`,
|
|
57
|
+
);
|
|
58
|
+
const lines = m[3] ? m[3].replace(/\n$/, '').split('\n') : [];
|
|
59
|
+
return {
|
|
60
|
+
schema: section.schema,
|
|
61
|
+
name: section.name,
|
|
62
|
+
table: m[1],
|
|
63
|
+
columns: m[2],
|
|
64
|
+
lines,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function decodeCopy(value) {
|
|
68
|
+
if (value === '\\N') return null;
|
|
69
|
+
return value.replace(/\\([0-7]{1,3}|x[0-9a-f]{1,2}|.)/gi, (_, code) => {
|
|
70
|
+
if (/^[0-7]/.test(code)) return String.fromCharCode(parseInt(code, 8));
|
|
71
|
+
if (/^x/i.test(code))
|
|
72
|
+
return String.fromCharCode(parseInt(code.slice(1), 16));
|
|
73
|
+
return (
|
|
74
|
+
{ t: '\t', n: '\n', r: '\r', b: '\b', f: '\f', v: '\v' }[code] || code
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function records(table) {
|
|
79
|
+
if (!table) return [];
|
|
80
|
+
const columns = table.columns
|
|
81
|
+
.split(/,\s*/)
|
|
82
|
+
.map((x) => x.replace(/^"|"$/g, '').replace(/""/g, '"'));
|
|
83
|
+
return table.lines.map((line) =>
|
|
84
|
+
Object.fromEntries(
|
|
85
|
+
line.split('\t').map((v, i) => [columns[i], decodeCopy(v)]),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
function textArray(value) {
|
|
90
|
+
if (value === null || value === undefined) return null;
|
|
91
|
+
if (Array.isArray(value)) return value;
|
|
92
|
+
if (!value.startsWith('{') || !value.endsWith('}'))
|
|
93
|
+
throw new Error('Invalid PostgreSQL text array');
|
|
94
|
+
if (value === '{}') return [];
|
|
95
|
+
const result = [];
|
|
96
|
+
let item = '',
|
|
97
|
+
quoted = false,
|
|
98
|
+
escaped = false;
|
|
99
|
+
for (const char of value.slice(1, -1)) {
|
|
100
|
+
if (escaped) {
|
|
101
|
+
item += char;
|
|
102
|
+
escaped = false;
|
|
103
|
+
} else if (char === '\\') escaped = true;
|
|
104
|
+
else if (char === '"') quoted = !quoted;
|
|
105
|
+
else if (char === ',' && !quoted) {
|
|
106
|
+
result.push(item);
|
|
107
|
+
item = '';
|
|
108
|
+
} else item += char;
|
|
109
|
+
}
|
|
110
|
+
if (quoted || escaped) throw new Error('Invalid PostgreSQL text array');
|
|
111
|
+
result.push(item);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function remapServiceUrls(text, source, target) {
|
|
115
|
+
if (!source || !target || source === target) return text;
|
|
116
|
+
for (const ref of [source, target])
|
|
117
|
+
if (!/^[a-z0-9-]+$/.test(ref)) throw new Error('Invalid project reference');
|
|
118
|
+
return text.replace(
|
|
119
|
+
new RegExp(
|
|
120
|
+
`(?<![a-z0-9.-])${source}\\.supabase\\.co(?=[:/\\\\"'\\s]|$)`,
|
|
121
|
+
'g',
|
|
122
|
+
),
|
|
123
|
+
`${target}.supabase.co`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function resetObjectAcl(schemas) {
|
|
127
|
+
const scope = schemas.map(literal).join(',');
|
|
128
|
+
return `DO $smoonb_acl$ DECLARE o record; g record; BEGIN
|
|
129
|
+
FOR o IN
|
|
130
|
+
SELECT 'TABLE' kind,format('%I.%I',n.nspname,c.relname) object,c.relowner owner,c.relacl acl,false public_default
|
|
131
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN (${scope}) AND c.relkind IN ('r','p','v','m','f')
|
|
132
|
+
UNION ALL SELECT 'SEQUENCE',format('%I.%I',n.nspname,c.relname),c.relowner,c.relacl,false FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN (${scope}) AND c.relkind='S'
|
|
133
|
+
UNION ALL SELECT 'FUNCTION',format('%I.%I(%s)',n.nspname,p.proname,pg_get_function_identity_arguments(p.oid)),p.proowner,p.proacl,true FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname IN (${scope}) AND p.prokind IN ('f','w')
|
|
134
|
+
LOOP
|
|
135
|
+
FOR g IN SELECT DISTINCT grantee FROM aclexplode(o.acl) WHERE grantee<>o.owner LOOP
|
|
136
|
+
EXECUTE format('REVOKE ALL ON %s %s FROM %s',o.kind,o.object,CASE WHEN g.grantee=0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(g.grantee)) END);
|
|
137
|
+
END LOOP;
|
|
138
|
+
IF o.public_default THEN EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO PUBLIC',o.object); END IF;
|
|
139
|
+
END LOOP;
|
|
140
|
+
END $smoonb_acl$;`;
|
|
141
|
+
}
|
|
142
|
+
const verifyForeignKeys = `DO $smoonb_fk$ DECLARE fk record; join_sql text; null_sql text; bad boolean; BEGIN
|
|
143
|
+
FOR fk IN SELECT c.* FROM pg_constraint c JOIN pg_namespace n ON n.oid=c.connamespace WHERE c.contype='f' AND n.nspname NOT LIKE 'pg_%' LOOP
|
|
144
|
+
SELECT string_agg(format('c.%I = p.%I',a.attname,b.attname),' AND '),string_agg(format('c.%I IS NOT NULL',a.attname),' AND ')
|
|
145
|
+
INTO join_sql,null_sql FROM unnest(fk.conkey,fk.confkey) WITH ORDINALITY k(a,b,i)
|
|
146
|
+
JOIN pg_attribute a ON a.attrelid=fk.conrelid AND a.attnum=k.a JOIN pg_attribute b ON b.attrelid=fk.confrelid AND b.attnum=k.b;
|
|
147
|
+
EXECUTE format('SELECT EXISTS(SELECT 1 FROM %s c WHERE %s AND NOT EXISTS(SELECT 1 FROM %s p WHERE %s))',fk.conrelid::regclass,null_sql,fk.confrelid::regclass,join_sql) INTO bad;
|
|
148
|
+
IF bad THEN RAISE EXCEPTION 'Foreign key verification failed: %',fk.conname; END IF;
|
|
149
|
+
END LOOP;
|
|
150
|
+
END $smoonb_fk$;`;
|
|
151
|
+
function buildRestore(
|
|
152
|
+
dump,
|
|
153
|
+
{ sourceProjectId, targetProjectId, remapUrls = true } = {},
|
|
154
|
+
) {
|
|
155
|
+
const { sections, tables } = dump;
|
|
156
|
+
const schemas = [
|
|
157
|
+
...new Set(sections.map((s) => s.schema).filter(isApplication)),
|
|
158
|
+
];
|
|
159
|
+
if (!schemas.length) throw new Error('No application schema in backup');
|
|
160
|
+
const sql = [
|
|
161
|
+
"BEGIN; SET LOCAL statement_timeout=0; SET LOCAL lock_timeout='30s'; SET LOCAL check_function_bodies=false; SET LOCAL session_replication_role=replica; SET LOCAL search_path='';",
|
|
162
|
+
];
|
|
163
|
+
const clean = (s) => s.sql.replace(/^\\.*$/gm, '');
|
|
164
|
+
const app = (s) =>
|
|
165
|
+
isApplication(s.schema) ||
|
|
166
|
+
(s.type === 'SCHEMA' && isApplication(s.name)) ||
|
|
167
|
+
(s.type === 'ACL' &&
|
|
168
|
+
s.name.startsWith('SCHEMA ') &&
|
|
169
|
+
isApplication(s.name.slice(7)));
|
|
170
|
+
const selected = sections.filter(
|
|
171
|
+
(s) =>
|
|
172
|
+
app(s) ||
|
|
173
|
+
s.type === 'EXTENSION' ||
|
|
174
|
+
(managed.has(s.schema) &&
|
|
175
|
+
(s.type === 'POLICY' ||
|
|
176
|
+
(s.type === 'TRIGGER' &&
|
|
177
|
+
/EXECUTE (?:FUNCTION|PROCEDURE) (?:public|"public")\./.test(
|
|
178
|
+
s.sql,
|
|
179
|
+
)))),
|
|
180
|
+
);
|
|
181
|
+
const late = new Set([
|
|
182
|
+
'TABLE DATA',
|
|
183
|
+
'SEQUENCE SET',
|
|
184
|
+
'ACL',
|
|
185
|
+
'DEFAULT ACL',
|
|
186
|
+
'FK CONSTRAINT',
|
|
187
|
+
'TRIGGER',
|
|
188
|
+
'POLICY',
|
|
189
|
+
'ROW SECURITY',
|
|
190
|
+
]);
|
|
191
|
+
for (const s of selected.filter((s) => !late.has(s.type))) sql.push(clean(s));
|
|
192
|
+
const restored = tables.filter(
|
|
193
|
+
(t) =>
|
|
194
|
+
isApplication(t.schema) ||
|
|
195
|
+
(t.schema === 'auth' && t.name !== 'schema_migrations') ||
|
|
196
|
+
(t.schema === 'storage' && ['objects', 'buckets'].includes(t.name)),
|
|
197
|
+
);
|
|
198
|
+
const unsupported = tables.filter(
|
|
199
|
+
(t) =>
|
|
200
|
+
t.lines.length &&
|
|
201
|
+
!restored.includes(t) &&
|
|
202
|
+
!['schema_migrations', 'migrations'].includes(t.name),
|
|
203
|
+
);
|
|
204
|
+
if (unsupported.length)
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Backup has data requiring a dedicated migration: ${unsupported.map((t) => t.table).join(', ')}`,
|
|
207
|
+
);
|
|
208
|
+
let changedRows = 0;
|
|
209
|
+
for (const [i, t] of restored.entries()) {
|
|
210
|
+
const temp = ident(`smoonb_expected_${i}`);
|
|
211
|
+
let lines = t.lines;
|
|
212
|
+
if (remapUrls && isApplication(t.schema))
|
|
213
|
+
lines = lines.map((line) => {
|
|
214
|
+
const v = remapServiceUrls(line, sourceProjectId, targetProjectId);
|
|
215
|
+
if (v !== line) changedRows++;
|
|
216
|
+
return v;
|
|
217
|
+
});
|
|
218
|
+
sql.push(
|
|
219
|
+
`DO $smoonb_empty$ BEGIN IF EXISTS(SELECT 1 FROM ${t.table}) THEN RAISE EXCEPTION USING MESSAGE=${literal('Destination must be empty: ' + t.table)}; END IF; END $smoonb_empty$;`,
|
|
220
|
+
);
|
|
221
|
+
sql.push(
|
|
222
|
+
`CREATE TEMP TABLE ${temp} ON COMMIT DROP AS SELECT ${t.columns} FROM ${t.table} WITH NO DATA;`,
|
|
223
|
+
);
|
|
224
|
+
sql.push(
|
|
225
|
+
`COPY ${temp} (${t.columns}) FROM stdin;\n${lines.length ? lines.join('\n') + '\n' : ''}\\.`,
|
|
226
|
+
);
|
|
227
|
+
sql.push(
|
|
228
|
+
`INSERT INTO ${t.table} (${t.columns}) OVERRIDING SYSTEM VALUE SELECT ${t.columns} FROM ${temp};`,
|
|
229
|
+
);
|
|
230
|
+
const actual = `SELECT to_jsonb(r) FROM (SELECT ${t.columns} FROM ${t.table}) r`;
|
|
231
|
+
const expected = `SELECT to_jsonb(r) FROM (SELECT ${t.columns} FROM ${temp}) r`;
|
|
232
|
+
sql.push(
|
|
233
|
+
`DO $smoonb_rows$ BEGIN IF EXISTS((${actual} EXCEPT ALL ${expected}) UNION ALL (${expected} EXCEPT ALL ${actual})) THEN RAISE EXCEPTION USING MESSAGE=${literal('Data verification failed: ' + t.table)}; END IF; END $smoonb_rows$;`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
for (const s of sections.filter(
|
|
237
|
+
(s) => s.type === 'SEQUENCE SET' && (app(s) || s.schema === 'auth'),
|
|
238
|
+
))
|
|
239
|
+
sql.push(clean(s));
|
|
240
|
+
for (const s of selected.filter((s) =>
|
|
241
|
+
['FK CONSTRAINT', 'TRIGGER', 'POLICY', 'ROW SECURITY'].includes(s.type),
|
|
242
|
+
))
|
|
243
|
+
sql.push(clean(s));
|
|
244
|
+
sql.push(resetObjectAcl(schemas));
|
|
245
|
+
// Supabase-managed administrator defaults are maintained by the platform;
|
|
246
|
+
// the customer postgres role cannot and must not impersonate supabase_admin.
|
|
247
|
+
for (const s of selected.filter(
|
|
248
|
+
(s) =>
|
|
249
|
+
s.type === 'ACL' ||
|
|
250
|
+
(s.type === 'DEFAULT ACL' && !s.owner.startsWith('supabase_')),
|
|
251
|
+
))
|
|
252
|
+
sql.push(clean(s));
|
|
253
|
+
for (const s of sections.filter((s) => s.type === 'PUBLICATION TABLE'))
|
|
254
|
+
sql.push(clean(s));
|
|
255
|
+
sql.push(
|
|
256
|
+
verifyForeignKeys,
|
|
257
|
+
'SET LOCAL session_replication_role=origin; COMMIT;',
|
|
258
|
+
);
|
|
259
|
+
return {
|
|
260
|
+
sql: sql.join('\n'),
|
|
261
|
+
summary: {
|
|
262
|
+
success: true,
|
|
263
|
+
verified: true,
|
|
264
|
+
tables: restored.length,
|
|
265
|
+
rows: restored.reduce((n, t) => n + t.lines.length, 0),
|
|
266
|
+
remapped_rows: changedRows,
|
|
267
|
+
managed_schema_ddl: 'preserved_destination',
|
|
268
|
+
verification:
|
|
269
|
+
'all restored columns (multiset) and foreign keys, in transaction',
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
module.exports = {
|
|
274
|
+
readDump,
|
|
275
|
+
parseCopy,
|
|
276
|
+
records,
|
|
277
|
+
decodeCopy,
|
|
278
|
+
textArray,
|
|
279
|
+
remapServiceUrls,
|
|
280
|
+
buildRestore,
|
|
281
|
+
managed,
|
|
282
|
+
isApplication,
|
|
283
|
+
verifyForeignKeys,
|
|
284
|
+
};
|
|
@@ -12,17 +12,11 @@ const { isPostgresMajorSupported } = require('../backup/utils');
|
|
|
12
12
|
const step00License = require('../backup/steps/00-license');
|
|
13
13
|
const step01DockerValidation = require('../backup/steps/01-docker-validation');
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const { runRestore } = require('./runner');
|
|
16
16
|
|
|
17
17
|
// Importar todas as etapas
|
|
18
18
|
const step00BackupSelection = require('./steps/00-backup-selection');
|
|
19
19
|
const step01ComponentsSelection = require('./steps/01-components-selection');
|
|
20
|
-
const step03Database = require('./steps/03-database');
|
|
21
|
-
const step04EdgeFunctions = require('./steps/04-edge-functions');
|
|
22
|
-
const step05AuthSettings = require('./steps/05-auth-settings');
|
|
23
|
-
const step06Storage = require('./steps/06-storage');
|
|
24
|
-
const step07DatabaseSettings = require('./steps/07-database-settings');
|
|
25
|
-
const step08RealtimeSettings = require('./steps/08-realtime-settings');
|
|
26
20
|
const { sendTelemetry } = require('../../telemetry');
|
|
27
21
|
|
|
28
22
|
module.exports = async (options = {}) => {
|
|
@@ -176,125 +170,9 @@ module.exports = async (options = {}) => {
|
|
|
176
170
|
process.exit(0);
|
|
177
171
|
}
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ui.info(` ${getT('restore.disclaimer.errorsReason')}`);
|
|
183
|
-
ui.info('');
|
|
184
|
-
ui.info(` ${getT('restore.disclaimer.supabaseDoc')}`);
|
|
185
|
-
ui.link(` ${SUPABASE_RESTORE_DOC_URL}`);
|
|
186
|
-
ui.info('');
|
|
187
|
-
ui.warn(` ${getT('restore.disclaimer.waitComplete')}`);
|
|
188
|
-
ui.warn(` ${getT('restore.disclaimer.testResult')}`);
|
|
189
|
-
ui.info('');
|
|
190
|
-
const acceptErrors = await confirm(getT('restore.disclaimer.acceptProceed'), true);
|
|
191
|
-
if (!acceptErrors) {
|
|
192
|
-
ui.warn(`🚫 ${getT('restore.index.restoreCancelled')}`);
|
|
193
|
-
process.exit(0);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 6. Executar restauração
|
|
197
|
-
ui.step(`\n🚀 ${getT('restore.index.startingRestore')}`);
|
|
198
|
-
|
|
199
|
-
// Contar etapas totais para numeração dinâmica
|
|
200
|
-
let stepNumber = 0;
|
|
201
|
-
const totalSteps = (components.database ? 1 : 0) +
|
|
202
|
-
(components.edgeFunctions ? 1 : 0) +
|
|
203
|
-
(components.authSettings ? 1 : 0) +
|
|
204
|
-
(components.storage ? 1 : 0) +
|
|
205
|
-
(components.databaseSettings ? 1 : 0) +
|
|
206
|
-
(components.realtimeSettings ? 1 : 0);
|
|
207
|
-
|
|
208
|
-
// Armazenar resultados para o resumo final
|
|
209
|
-
const restoreResults = {};
|
|
210
|
-
|
|
211
|
-
// 6.1 Database (se selecionado)
|
|
212
|
-
if (components.database) {
|
|
213
|
-
stepNumber++;
|
|
214
|
-
ui.step(`\n📊 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringDatabase')}`);
|
|
215
|
-
await step03Database({
|
|
216
|
-
backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
|
|
217
|
-
targetDatabaseUrl: targetProject.targetDatabaseUrl
|
|
218
|
-
});
|
|
219
|
-
restoreResults.database = { success: true };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 6.2 Edge Functions (se selecionado)
|
|
223
|
-
if (components.edgeFunctions) {
|
|
224
|
-
stepNumber++;
|
|
225
|
-
ui.step(`\n⚡ ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringEdgeFunctions')}`);
|
|
226
|
-
const edgeFunctionsResult = await step04EdgeFunctions({
|
|
227
|
-
backupPath: selectedBackup.path,
|
|
228
|
-
targetProject
|
|
229
|
-
});
|
|
230
|
-
restoreResults.edgeFunctions = edgeFunctionsResult || { success: true };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 6.3 Auth Settings (se selecionado)
|
|
234
|
-
if (components.authSettings) {
|
|
235
|
-
stepNumber++;
|
|
236
|
-
ui.step(`\n🔐 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringAuthSettings')}`);
|
|
237
|
-
await step05AuthSettings({
|
|
238
|
-
backupPath: selectedBackup.path,
|
|
239
|
-
targetProject
|
|
240
|
-
});
|
|
241
|
-
restoreResults.authSettings = { success: true };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 6.4 Storage Buckets (se selecionado)
|
|
245
|
-
if (components.storage) {
|
|
246
|
-
stepNumber++;
|
|
247
|
-
ui.step(`\n📦 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringStorageBuckets')}`);
|
|
248
|
-
const storageResult = await step06Storage({
|
|
249
|
-
backupPath: selectedBackup.path,
|
|
250
|
-
targetProject
|
|
251
|
-
});
|
|
252
|
-
restoreResults.storage = storageResult || { success: true };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// 6.5 Database Settings (se selecionado)
|
|
256
|
-
if (components.databaseSettings) {
|
|
257
|
-
stepNumber++;
|
|
258
|
-
ui.step(`\n🔧 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringDatabaseSettings')}`);
|
|
259
|
-
await step07DatabaseSettings({
|
|
260
|
-
backupPath: selectedBackup.path,
|
|
261
|
-
targetProject
|
|
262
|
-
});
|
|
263
|
-
restoreResults.databaseSettings = { success: true };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 6.6 Realtime Settings (se selecionado)
|
|
267
|
-
if (components.realtimeSettings) {
|
|
268
|
-
stepNumber++;
|
|
269
|
-
ui.step(`\n🔄 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringRealtimeSettings')}`);
|
|
270
|
-
await step08RealtimeSettings({
|
|
271
|
-
backupPath: selectedBackup.path,
|
|
272
|
-
targetProject
|
|
273
|
-
});
|
|
274
|
-
restoreResults.realtimeSettings = { success: true };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// report.json de restauração
|
|
278
|
-
const report = {
|
|
279
|
-
process: 'restore',
|
|
280
|
-
created_at: new Date().toISOString(),
|
|
281
|
-
target_project_id: targetProject.targetProjectId,
|
|
282
|
-
assets: {
|
|
283
|
-
env: path.join(processDir, 'env', '.env.local'),
|
|
284
|
-
env_map: path.join(processDir, 'env', 'env-map.json')
|
|
285
|
-
},
|
|
286
|
-
components: components,
|
|
287
|
-
results: restoreResults,
|
|
288
|
-
notes: [
|
|
289
|
-
'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
|
|
290
|
-
]
|
|
291
|
-
};
|
|
292
|
-
try {
|
|
293
|
-
fs.writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
|
|
294
|
-
} catch {
|
|
295
|
-
// silencioso
|
|
296
|
-
}
|
|
297
|
-
|
|
173
|
+
const remapUrls = await confirm(getT('restore.verified.remapUrls'), true);
|
|
174
|
+
const restoreResults = await runRestore({ selectedBackup, components, targetProject, processDir, postgresMajor:parsedMajor, remapUrls });
|
|
175
|
+
|
|
298
176
|
// Exibir resumo final
|
|
299
177
|
ui.success(`\n🎉 ${getT('restore.index.restoreComplete')}`);
|
|
300
178
|
ui.link(`🎯 ${getT('restore.index.targetProjectFinal', { projectId: targetProject.targetProjectId || getT('restore.index.notConfigured') })}`);
|