@hot-updater/supabase 0.35.8 → 0.35.9
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/iac/index.cjs +733 -85
- package/dist/iac/index.d.cts +78 -13
- package/dist/iac/index.d.mts +78 -13
- package/dist/iac/index.mjs +732 -87
- package/dist/init/index.cjs +112 -0
- package/dist/init/index.d.cts +89 -0
- package/dist/init/index.d.mts +89 -0
- package/dist/init/index.mjs +107 -0
- package/package.json +14 -9
- package/supabase/edge-functions/runtime.docker.integration.spec.ts +25 -21
package/dist/iac/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { ConfigBuilder, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, link, makeEnv, p, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
|
|
4
|
+
import { ConfigBuilder, InitError, MissingInitInputsError, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveInitProviderInput, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -6271,6 +6271,112 @@ createExeca(mapNode);
|
|
|
6271
6271
|
createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
|
|
6272
6272
|
const { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcExport();
|
|
6273
6273
|
//#endregion
|
|
6274
|
+
//#region iac/init/index.ts
|
|
6275
|
+
const SUPABASE_DATABASE_PASSWORD_PROJECT_ID_ENV_KEY = "HOT_UPDATER_SUPABASE_DB_PASSWORD_PROJECT_ID";
|
|
6276
|
+
const SUPABASE_REGION_VALUES = [
|
|
6277
|
+
"ap-east-1",
|
|
6278
|
+
"ap-northeast-1",
|
|
6279
|
+
"ap-northeast-2",
|
|
6280
|
+
"ap-south-1",
|
|
6281
|
+
"ap-southeast-1",
|
|
6282
|
+
"ap-southeast-2",
|
|
6283
|
+
"ca-central-1",
|
|
6284
|
+
"eu-central-1",
|
|
6285
|
+
"eu-central-2",
|
|
6286
|
+
"eu-north-1",
|
|
6287
|
+
"eu-west-1",
|
|
6288
|
+
"eu-west-2",
|
|
6289
|
+
"eu-west-3",
|
|
6290
|
+
"sa-east-1",
|
|
6291
|
+
"us-east-1",
|
|
6292
|
+
"us-east-2",
|
|
6293
|
+
"us-west-1",
|
|
6294
|
+
"us-west-2"
|
|
6295
|
+
];
|
|
6296
|
+
const isSupabaseRegion = (value) => value !== void 0 && SUPABASE_REGION_VALUES.some((region) => region === value);
|
|
6297
|
+
const isSupabaseFunctionName = (value) => value !== void 0 && /^[A-Za-z][A-Za-z0-9_-]*$/.test(value);
|
|
6298
|
+
const initProvider = {
|
|
6299
|
+
label: "Supabase",
|
|
6300
|
+
inputs: {
|
|
6301
|
+
projectId: {
|
|
6302
|
+
envKey: "HOT_UPDATER_SUPABASE_PROJECT_ID",
|
|
6303
|
+
help: "Supabase project reference"
|
|
6304
|
+
},
|
|
6305
|
+
projectName: {
|
|
6306
|
+
envKey: "HOT_UPDATER_SUPABASE_PROJECT_NAME",
|
|
6307
|
+
help: "Project name used when creating a Supabase project",
|
|
6308
|
+
optional: true,
|
|
6309
|
+
prompt: {
|
|
6310
|
+
defaultValue: "hot-updater",
|
|
6311
|
+
message: "Enter a name for the new Supabase project",
|
|
6312
|
+
placeholder: "hot-updater",
|
|
6313
|
+
type: "text"
|
|
6314
|
+
}
|
|
6315
|
+
},
|
|
6316
|
+
organizationSlug: {
|
|
6317
|
+
envKey: "HOT_UPDATER_SUPABASE_ORGANIZATION_SLUG",
|
|
6318
|
+
help: "Organization slug used when creating a Supabase project",
|
|
6319
|
+
optional: true,
|
|
6320
|
+
prompt: {
|
|
6321
|
+
message: "Select a Supabase organization",
|
|
6322
|
+
type: "select"
|
|
6323
|
+
}
|
|
6324
|
+
},
|
|
6325
|
+
region: {
|
|
6326
|
+
envKey: "HOT_UPDATER_SUPABASE_REGION",
|
|
6327
|
+
help: "Region used when creating a Supabase project",
|
|
6328
|
+
optional: true,
|
|
6329
|
+
prompt: {
|
|
6330
|
+
defaultValue: "us-east-1",
|
|
6331
|
+
message: "Select a region for the new Supabase project",
|
|
6332
|
+
type: "select"
|
|
6333
|
+
},
|
|
6334
|
+
validate: isSupabaseRegion
|
|
6335
|
+
},
|
|
6336
|
+
accessToken: {
|
|
6337
|
+
envKey: "SUPABASE_ACCESS_TOKEN",
|
|
6338
|
+
help: "Supabase personal access token",
|
|
6339
|
+
persistence: "with-consent",
|
|
6340
|
+
preflight: false,
|
|
6341
|
+
prompt: {
|
|
6342
|
+
message: "Enter your Supabase personal access token",
|
|
6343
|
+
type: "password"
|
|
6344
|
+
}
|
|
6345
|
+
},
|
|
6346
|
+
bucketName: {
|
|
6347
|
+
envKey: "HOT_UPDATER_SUPABASE_BUCKET_NAME",
|
|
6348
|
+
help: "Storage bucket name",
|
|
6349
|
+
prompt: {
|
|
6350
|
+
defaultValue: "hot-updater-storage",
|
|
6351
|
+
message: "Enter a name for the new storage bucket",
|
|
6352
|
+
placeholder: "hot-updater-storage",
|
|
6353
|
+
type: "text"
|
|
6354
|
+
}
|
|
6355
|
+
},
|
|
6356
|
+
functionName: {
|
|
6357
|
+
envKey: "HOT_UPDATER_SUPABASE_FUNCTION_NAME",
|
|
6358
|
+
help: "Edge Function name",
|
|
6359
|
+
prompt: {
|
|
6360
|
+
defaultValue: "update-server",
|
|
6361
|
+
message: "Enter a name for the edge function",
|
|
6362
|
+
placeholder: "update-server",
|
|
6363
|
+
type: "text"
|
|
6364
|
+
},
|
|
6365
|
+
validate: isSupabaseFunctionName
|
|
6366
|
+
},
|
|
6367
|
+
databasePassword: {
|
|
6368
|
+
envKey: "HOT_UPDATER_SUPABASE_DB_PASSWORD",
|
|
6369
|
+
help: "Database password, when required by the linked project",
|
|
6370
|
+
optional: true,
|
|
6371
|
+
persistence: "with-consent",
|
|
6372
|
+
prompt: {
|
|
6373
|
+
message: "Enter your Supabase database password (press Enter to skip if none)",
|
|
6374
|
+
type: "password"
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
};
|
|
6379
|
+
//#endregion
|
|
6274
6380
|
//#region iac/supabaseApi.ts
|
|
6275
6381
|
const supabaseApi = (supabaseUrl, supabaseServiceRoleKey) => {
|
|
6276
6382
|
const supabase = createClient(supabaseUrl, supabaseServiceRoleKey);
|
|
@@ -6289,10 +6395,121 @@ const supabaseApi = (supabaseUrl, supabaseServiceRoleKey) => {
|
|
|
6289
6395
|
const { data, error } = await supabase.storage.createBucket(bucketName, options);
|
|
6290
6396
|
if (error) throw error;
|
|
6291
6397
|
return data;
|
|
6398
|
+
},
|
|
6399
|
+
updateBucket: async (bucketId, options) => {
|
|
6400
|
+
const { error } = await supabase.storage.updateBucket(bucketId, options);
|
|
6401
|
+
if (error) throw error;
|
|
6292
6402
|
}
|
|
6293
6403
|
};
|
|
6294
6404
|
};
|
|
6295
6405
|
//#endregion
|
|
6406
|
+
//#region iac/supabaseAuthentication.ts
|
|
6407
|
+
const SUPABASE_AUTH_METHOD = {
|
|
6408
|
+
accessToken: "access-token",
|
|
6409
|
+
cliLogin: "cli-login"
|
|
6410
|
+
};
|
|
6411
|
+
const SUPABASE_LOGIN_URL_PATTERN = /https:\/\/supabase\.com\/dashboard\/cli\/login\?[A-Za-z0-9._~!$&'()*+,;=:@/?%-]+/;
|
|
6412
|
+
const openBrowser = async (url) => {
|
|
6413
|
+
if (process.platform === "darwin") {
|
|
6414
|
+
await execa("open", [url]);
|
|
6415
|
+
return;
|
|
6416
|
+
}
|
|
6417
|
+
if (process.platform === "win32") {
|
|
6418
|
+
await execa("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
|
|
6419
|
+
return;
|
|
6420
|
+
}
|
|
6421
|
+
await execa("xdg-open", [url]);
|
|
6422
|
+
};
|
|
6423
|
+
const hasValidSupabaseCliLogin = async () => {
|
|
6424
|
+
return (await execa("npx", [
|
|
6425
|
+
"-y",
|
|
6426
|
+
"supabase",
|
|
6427
|
+
"projects",
|
|
6428
|
+
"list",
|
|
6429
|
+
"--output",
|
|
6430
|
+
"json",
|
|
6431
|
+
"--agent",
|
|
6432
|
+
"no"
|
|
6433
|
+
], { reject: false })).exitCode === 0;
|
|
6434
|
+
};
|
|
6435
|
+
const getSupabaseCliEnv = (accessToken) => accessToken ? { [initProvider.inputs.accessToken.envKey]: accessToken } : void 0;
|
|
6436
|
+
const inputSupabaseAccessToken = async (accessToken) => {
|
|
6437
|
+
if (accessToken) return accessToken;
|
|
6438
|
+
const authMethod = await p.select({
|
|
6439
|
+
message: "How do you want to authenticate with Supabase?",
|
|
6440
|
+
options: [{
|
|
6441
|
+
label: "Use Supabase CLI login",
|
|
6442
|
+
value: SUPABASE_AUTH_METHOD.cliLogin
|
|
6443
|
+
}, {
|
|
6444
|
+
label: "Enter a personal access token",
|
|
6445
|
+
value: SUPABASE_AUTH_METHOD.accessToken
|
|
6446
|
+
}]
|
|
6447
|
+
});
|
|
6448
|
+
if (p.isCancel(authMethod)) process.exit(0);
|
|
6449
|
+
if (authMethod === SUPABASE_AUTH_METHOD.cliLogin) {
|
|
6450
|
+
if (await hasValidSupabaseCliLogin()) return;
|
|
6451
|
+
const loginProcess = execa("npx", [
|
|
6452
|
+
"-y",
|
|
6453
|
+
"supabase",
|
|
6454
|
+
"login",
|
|
6455
|
+
"--no-browser",
|
|
6456
|
+
"--agent",
|
|
6457
|
+
"no"
|
|
6458
|
+
], {
|
|
6459
|
+
stdin: "inherit",
|
|
6460
|
+
stderr: "inherit",
|
|
6461
|
+
stdout: "pipe"
|
|
6462
|
+
});
|
|
6463
|
+
let output = "";
|
|
6464
|
+
let browserOpenPromise;
|
|
6465
|
+
loginProcess.stdout?.on("data", (chunk) => {
|
|
6466
|
+
process.stdout.write(chunk);
|
|
6467
|
+
output += chunk.toString();
|
|
6468
|
+
const loginUrl = output.match(SUPABASE_LOGIN_URL_PATTERN)?.[0];
|
|
6469
|
+
if (loginUrl && !browserOpenPromise) browserOpenPromise = openBrowser(loginUrl).catch(() => {
|
|
6470
|
+
p.log.warn("Could not open the Supabase login page automatically.");
|
|
6471
|
+
});
|
|
6472
|
+
});
|
|
6473
|
+
await loginProcess;
|
|
6474
|
+
await browserOpenPromise;
|
|
6475
|
+
return;
|
|
6476
|
+
}
|
|
6477
|
+
p.log.step(`Personal access token: ${link("https://supabase.com/dashboard/account/tokens")}`);
|
|
6478
|
+
const selectedAccessToken = await p.password({
|
|
6479
|
+
message: "Enter your Supabase personal access token",
|
|
6480
|
+
validate: (value) => value ? void 0 : "Supabase access token is required"
|
|
6481
|
+
});
|
|
6482
|
+
if (p.isCancel(selectedAccessToken)) process.exit(0);
|
|
6483
|
+
return selectedAccessToken;
|
|
6484
|
+
};
|
|
6485
|
+
//#endregion
|
|
6486
|
+
//#region iac/supabaseBucketPrivacy.ts
|
|
6487
|
+
var PublicSupabaseBucketError = class extends InitError {
|
|
6488
|
+
name = "PublicSupabaseBucketError";
|
|
6489
|
+
constructor(bucketName) {
|
|
6490
|
+
super([
|
|
6491
|
+
`Supabase bucket "${bucketName}" is public.`,
|
|
6492
|
+
"Make the bucket private in Supabase Storage, then rerun init.",
|
|
6493
|
+
"Alternatively, rerun without --env-file to approve the change interactively."
|
|
6494
|
+
].join("\n"));
|
|
6495
|
+
this.bucketName = bucketName;
|
|
6496
|
+
}
|
|
6497
|
+
};
|
|
6498
|
+
const ensureSupabaseBucketPrivate = async ({ api, nonInteractive, selection }) => {
|
|
6499
|
+
if (selection.create || !selection.isPublic) return;
|
|
6500
|
+
if (nonInteractive) throw new PublicSupabaseBucketError(selection.name);
|
|
6501
|
+
const confirmed = await p.confirm({
|
|
6502
|
+
message: `Bucket "${selection.name}" is public. Make it private?`,
|
|
6503
|
+
initialValue: true
|
|
6504
|
+
});
|
|
6505
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
6506
|
+
p.log.info("Init cancelled.");
|
|
6507
|
+
process.exit(1);
|
|
6508
|
+
}
|
|
6509
|
+
await api.updateBucket(selection.id, { public: false });
|
|
6510
|
+
p.log.success(`Bucket "${selection.name}" is now private.`);
|
|
6511
|
+
};
|
|
6512
|
+
//#endregion
|
|
6296
6513
|
//#region iac/supabaseCli.ts
|
|
6297
6514
|
const SUPABASE_CONFIG_TEMPLATE = `
|
|
6298
6515
|
project_id = "%%projectId%%"
|
|
@@ -6307,6 +6524,10 @@ const SUPABASE_DATABASE_AUTH_ERROR_PATTERNS = [
|
|
|
6307
6524
|
/SQLSTATE 28P01/i,
|
|
6308
6525
|
/invalid SCRAM server-final-message/i
|
|
6309
6526
|
];
|
|
6527
|
+
const getSupabaseCommandEnv = (accessToken, dbPassword) => accessToken || dbPassword ? {
|
|
6528
|
+
...getSupabaseCliEnv(accessToken),
|
|
6529
|
+
...dbPassword ? { SUPABASE_DB_PASSWORD: dbPassword } : {}
|
|
6530
|
+
} : void 0;
|
|
6310
6531
|
const isSupabaseDatabaseAuthError = (err) => {
|
|
6311
6532
|
const stderr = err.stderr;
|
|
6312
6533
|
return typeof stderr === "string" && SUPABASE_DATABASE_AUTH_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
@@ -6314,11 +6535,18 @@ const isSupabaseDatabaseAuthError = (err) => {
|
|
|
6314
6535
|
const handleSupabaseDatabaseCommandError = (err, { dbPassword, stderrInherited = false }) => {
|
|
6315
6536
|
if (err instanceof ExecaError) if (dbPassword && isSupabaseDatabaseAuthError(err)) p.log.error(SUPABASE_DATABASE_CONNECTION_ERROR);
|
|
6316
6537
|
else if (!stderrInherited && err.stderr) p.log.error(err.stderr);
|
|
6317
|
-
else
|
|
6538
|
+
else p.log.error(err.message);
|
|
6318
6539
|
else console.error(err);
|
|
6319
6540
|
process.exit(1);
|
|
6320
6541
|
};
|
|
6321
|
-
const
|
|
6542
|
+
const confirmSupabaseDatabaseMigrations = async ({ nonInteractive }) => {
|
|
6543
|
+
if (nonInteractive) return true;
|
|
6544
|
+
return await p.confirm({
|
|
6545
|
+
message: "Apply Hot Updater database migrations to the selected Supabase project?",
|
|
6546
|
+
initialValue: true
|
|
6547
|
+
}) === true;
|
|
6548
|
+
};
|
|
6549
|
+
const linkSupabase = async (workdir, { accessToken, projectId, dbPassword }) => {
|
|
6322
6550
|
const spinner = p.spinner();
|
|
6323
6551
|
try {
|
|
6324
6552
|
await fs.writeFile(path.join(workdir, "supabase", "config.toml"), transformTemplate(SUPABASE_CONFIG_TEMPLATE, { projectId }));
|
|
@@ -6332,7 +6560,7 @@ const linkSupabase = async (workdir, { projectId, dbPassword }) => {
|
|
|
6332
6560
|
workdir
|
|
6333
6561
|
], {
|
|
6334
6562
|
cwd: workdir,
|
|
6335
|
-
env:
|
|
6563
|
+
env: getSupabaseCommandEnv(accessToken, dbPassword),
|
|
6336
6564
|
input: "",
|
|
6337
6565
|
stdio: [
|
|
6338
6566
|
"pipe",
|
|
@@ -6343,19 +6571,20 @@ const linkSupabase = async (workdir, { projectId, dbPassword }) => {
|
|
|
6343
6571
|
spinner.stop("Supabase linked ✔");
|
|
6344
6572
|
} catch (err) {
|
|
6345
6573
|
spinner.stop();
|
|
6346
|
-
handleSupabaseDatabaseCommandError(err, { dbPassword });
|
|
6574
|
+
handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), { dbPassword });
|
|
6347
6575
|
}
|
|
6348
6576
|
};
|
|
6349
|
-
const pushDB = async (workdir, { dbPassword }) => {
|
|
6577
|
+
const pushDB = async (workdir, { accessToken, dbPassword }) => {
|
|
6350
6578
|
try {
|
|
6351
6579
|
const dbPush = await execa("npx", [
|
|
6352
6580
|
"supabase",
|
|
6353
6581
|
"db",
|
|
6354
6582
|
"push",
|
|
6355
|
-
"--include-all"
|
|
6583
|
+
"--include-all",
|
|
6584
|
+
"--yes"
|
|
6356
6585
|
], {
|
|
6357
6586
|
cwd: workdir,
|
|
6358
|
-
env:
|
|
6587
|
+
env: getSupabaseCommandEnv(accessToken, dbPassword),
|
|
6359
6588
|
stderr: ["pipe", "inherit"],
|
|
6360
6589
|
stdin: "inherit",
|
|
6361
6590
|
stdout: "inherit"
|
|
@@ -6363,17 +6592,273 @@ const pushDB = async (workdir, { dbPassword }) => {
|
|
|
6363
6592
|
p.log.success("DB pushed ✔");
|
|
6364
6593
|
return dbPush.stdout;
|
|
6365
6594
|
} catch (err) {
|
|
6366
|
-
handleSupabaseDatabaseCommandError(err, {
|
|
6595
|
+
handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), {
|
|
6367
6596
|
dbPassword,
|
|
6368
6597
|
stderrInherited: true
|
|
6369
6598
|
});
|
|
6370
6599
|
}
|
|
6371
6600
|
};
|
|
6372
6601
|
//#endregion
|
|
6602
|
+
//#region iac/supabaseDeploymentInputs.ts
|
|
6603
|
+
const assertSupabaseNonInteractiveInputs = async (inputs, nonInteractive, validateCliLogin = hasValidSupabaseCliLogin) => {
|
|
6604
|
+
assertInitProviderInputs({
|
|
6605
|
+
inputs: nonInteractive && inputs.accessToken === void 0 && await validateCliLogin() ? {
|
|
6606
|
+
...inputs,
|
|
6607
|
+
accessToken: "supabase-cli-login"
|
|
6608
|
+
} : inputs,
|
|
6609
|
+
provider: initProvider,
|
|
6610
|
+
strict: nonInteractive
|
|
6611
|
+
});
|
|
6612
|
+
};
|
|
6613
|
+
const inputSupabaseDeploymentInputs = async ({ accessToken, functionName, nonInteractive }) => {
|
|
6614
|
+
const { inputs } = initProvider;
|
|
6615
|
+
assertInitInputs({
|
|
6616
|
+
inputs: { [inputs.functionName.envKey]: functionName },
|
|
6617
|
+
strict: nonInteractive
|
|
6618
|
+
});
|
|
6619
|
+
if (nonInteractive && functionName) return {
|
|
6620
|
+
accessToken,
|
|
6621
|
+
functionName
|
|
6622
|
+
};
|
|
6623
|
+
const savedFunctionName = isSupabaseFunctionName(functionName) ? functionName : void 0;
|
|
6624
|
+
return p.group({
|
|
6625
|
+
accessToken: () => nonInteractive && accessToken ? Promise.resolve(accessToken) : inputSupabaseAccessToken(),
|
|
6626
|
+
functionName: () => nonInteractive && savedFunctionName ? Promise.resolve(savedFunctionName) : p.text({
|
|
6627
|
+
...getInitProviderTextPromptValues(inputs.functionName.prompt, savedFunctionName),
|
|
6628
|
+
message: inputs.functionName.prompt.message,
|
|
6629
|
+
validate: (value) => isSupabaseFunctionName(value) ? void 0 : "Start with a letter and use only letters, numbers, underscores, or hyphens"
|
|
6630
|
+
})
|
|
6631
|
+
}, { onCancel: () => process.exit(0) });
|
|
6632
|
+
};
|
|
6633
|
+
//#endregion
|
|
6634
|
+
//#region iac/supabaseInitInputs.ts
|
|
6635
|
+
const resolveSupabaseInitInputs = (existingEnv, sources = {}) => {
|
|
6636
|
+
const { inputs } = initProvider;
|
|
6637
|
+
const managedEnv = sources.managedEnv ?? existingEnv;
|
|
6638
|
+
const projectId = resolveInitProviderInput(existingEnv, inputs.projectId) ?? getHotUpdaterEnvValue(existingEnv, "HOT_UPDATER_SUPABASE_URL")?.match(/^https:\/\/([^.]+)\.supabase\.co/)?.[1];
|
|
6639
|
+
const databasePasswordKey = inputs.databasePassword.envKey;
|
|
6640
|
+
const hasProcessDatabasePassword = process.env[databasePasswordKey] !== void 0;
|
|
6641
|
+
const hasInputDatabasePassword = sources.inputEnv !== void 0 && Object.hasOwn(sources.inputEnv, databasePasswordKey);
|
|
6642
|
+
const managedDatabasePasswordProjectId = managedEnv[SUPABASE_DATABASE_PASSWORD_PROJECT_ID_ENV_KEY]?.trim();
|
|
6643
|
+
const region = resolveInitProviderInput(existingEnv, inputs.region);
|
|
6644
|
+
const databasePassword = hasProcessDatabasePassword ? process.env[databasePasswordKey]?.trim() || void 0 : hasInputDatabasePassword ? sources.inputEnv?.[databasePasswordKey]?.trim() || void 0 : managedDatabasePasswordProjectId === projectId ? managedEnv[databasePasswordKey]?.trim() || void 0 : void 0;
|
|
6645
|
+
return {
|
|
6646
|
+
accessToken: resolveInitProviderInput(existingEnv, inputs.accessToken),
|
|
6647
|
+
bucketName: resolveInitProviderInput(existingEnv, inputs.bucketName),
|
|
6648
|
+
databasePassword,
|
|
6649
|
+
functionName: resolveInitProviderInput(existingEnv, inputs.functionName),
|
|
6650
|
+
organizationSlug: resolveInitProviderInput(existingEnv, inputs.organizationSlug),
|
|
6651
|
+
projectId,
|
|
6652
|
+
projectName: resolveInitProviderInput(existingEnv, inputs.projectName),
|
|
6653
|
+
region: isSupabaseRegion(region) ? region : void 0
|
|
6654
|
+
};
|
|
6655
|
+
};
|
|
6656
|
+
const inputSupabaseDatabasePassword = async ({ cliHandlesPrompt = false, databasePassword, nonInteractive, required = false }) => {
|
|
6657
|
+
if (cliHandlesPrompt) return "";
|
|
6658
|
+
if (nonInteractive) {
|
|
6659
|
+
if (required && !databasePassword) throw new MissingInitInputsError([initProvider.inputs.databasePassword.envKey]);
|
|
6660
|
+
return databasePassword ?? "";
|
|
6661
|
+
}
|
|
6662
|
+
const password = await p.password({
|
|
6663
|
+
message: initProvider.inputs.databasePassword.prompt.message,
|
|
6664
|
+
validate: (value) => required && !value ? "A database password is required to create a Supabase project" : void 0
|
|
6665
|
+
});
|
|
6666
|
+
if (p.isCancel(password)) process.exit(0);
|
|
6667
|
+
return password;
|
|
6668
|
+
};
|
|
6669
|
+
const inputSupabaseProjectCreationInputs = async ({ bucketName, organizationSlug, organizations, projectName, region }) => {
|
|
6670
|
+
if (organizations.length === 0) throw new Error("No Supabase organization is available for project creation.");
|
|
6671
|
+
const savedOrganization = organizations.find((organization) => organization.slug === organizationSlug);
|
|
6672
|
+
if (organizationSlug && !savedOrganization) p.log.warn("Saved Supabase organization was not found. Select an organization again.");
|
|
6673
|
+
const defaultRegion = initProvider.inputs.region.prompt.defaultValue;
|
|
6674
|
+
const selectedProjectName = await p.text({
|
|
6675
|
+
...getInitProviderTextPromptValues(initProvider.inputs.projectName.prompt, projectName),
|
|
6676
|
+
message: initProvider.inputs.projectName.prompt.message,
|
|
6677
|
+
validate: (value) => value ? void 0 : "Supabase project name is required"
|
|
6678
|
+
});
|
|
6679
|
+
if (p.isCancel(selectedProjectName)) process.exit(0);
|
|
6680
|
+
const selectedOrganizationSlug = await p.select({
|
|
6681
|
+
initialValue: savedOrganization?.slug ?? organizations[0]?.slug,
|
|
6682
|
+
message: initProvider.inputs.organizationSlug.prompt.message,
|
|
6683
|
+
options: organizations.map((organization) => ({
|
|
6684
|
+
label: organization.name,
|
|
6685
|
+
value: organization.slug
|
|
6686
|
+
}))
|
|
6687
|
+
});
|
|
6688
|
+
if (p.isCancel(selectedOrganizationSlug)) process.exit(0);
|
|
6689
|
+
const selectedRegion = await p.select({
|
|
6690
|
+
message: initProvider.inputs.region.prompt.message,
|
|
6691
|
+
initialValue: region ?? (isSupabaseRegion(defaultRegion) ? defaultRegion : void 0),
|
|
6692
|
+
options: SUPABASE_REGION_VALUES.map((value) => ({
|
|
6693
|
+
label: value,
|
|
6694
|
+
value
|
|
6695
|
+
}))
|
|
6696
|
+
});
|
|
6697
|
+
if (p.isCancel(selectedRegion)) process.exit(0);
|
|
6698
|
+
const selectedBucketName = await p.text({
|
|
6699
|
+
...getInitProviderTextPromptValues(initProvider.inputs.bucketName.prompt, bucketName),
|
|
6700
|
+
message: initProvider.inputs.bucketName.prompt.message,
|
|
6701
|
+
validate: (value) => value ? void 0 : "Storage bucket name is required"
|
|
6702
|
+
});
|
|
6703
|
+
if (p.isCancel(selectedBucketName)) process.exit(0);
|
|
6704
|
+
return {
|
|
6705
|
+
bucketName: selectedBucketName,
|
|
6706
|
+
organizationSlug: selectedOrganizationSlug,
|
|
6707
|
+
projectName: selectedProjectName,
|
|
6708
|
+
region: selectedRegion
|
|
6709
|
+
};
|
|
6710
|
+
};
|
|
6711
|
+
//#endregion
|
|
6712
|
+
//#region iac/supabaseCliManagementApi.ts
|
|
6713
|
+
var SupabaseCliResponseError = class extends Error {
|
|
6714
|
+
constructor(message) {
|
|
6715
|
+
super(message);
|
|
6716
|
+
this.name = "SupabaseCliResponseError";
|
|
6717
|
+
}
|
|
6718
|
+
};
|
|
6719
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
6720
|
+
const runSupabaseCli = async (args) => {
|
|
6721
|
+
const result = await execa("npx", [
|
|
6722
|
+
"-y",
|
|
6723
|
+
"supabase",
|
|
6724
|
+
...args
|
|
6725
|
+
], { env: void 0 });
|
|
6726
|
+
return JSON.parse(result.stdout);
|
|
6727
|
+
};
|
|
6728
|
+
const supabaseCliManagementApi = () => ({
|
|
6729
|
+
listOrganizations: async () => {
|
|
6730
|
+
const body = await runSupabaseCli([
|
|
6731
|
+
"orgs",
|
|
6732
|
+
"list",
|
|
6733
|
+
"--output",
|
|
6734
|
+
"json"
|
|
6735
|
+
]);
|
|
6736
|
+
if (!Array.isArray(body)) throw new SupabaseCliResponseError("Supabase organizations response was invalid.");
|
|
6737
|
+
return body.flatMap((organization) => isRecord$1(organization) && typeof organization.id === "string" && typeof organization.name === "string" && typeof organization.slug === "string" ? [{
|
|
6738
|
+
id: organization.id,
|
|
6739
|
+
name: organization.name,
|
|
6740
|
+
slug: organization.slug
|
|
6741
|
+
}] : []);
|
|
6742
|
+
},
|
|
6743
|
+
createProject: async ({ name, organizationSlug, region }) => {
|
|
6744
|
+
await execa("npx", [
|
|
6745
|
+
"-y",
|
|
6746
|
+
"supabase",
|
|
6747
|
+
"projects",
|
|
6748
|
+
"create",
|
|
6749
|
+
name,
|
|
6750
|
+
"--org-id",
|
|
6751
|
+
organizationSlug,
|
|
6752
|
+
"--region",
|
|
6753
|
+
region,
|
|
6754
|
+
"--agent",
|
|
6755
|
+
"no"
|
|
6756
|
+
], { stdio: "inherit" });
|
|
6757
|
+
const body = await runSupabaseCli([
|
|
6758
|
+
"projects",
|
|
6759
|
+
"list",
|
|
6760
|
+
"--output",
|
|
6761
|
+
"json"
|
|
6762
|
+
]);
|
|
6763
|
+
if (!Array.isArray(body)) throw new SupabaseCliResponseError("Supabase projects response was invalid after project creation.");
|
|
6764
|
+
const project = body.find((candidate) => isRecord$1(candidate) && candidate.name === name && candidate.organization_slug === organizationSlug && candidate.region === region);
|
|
6765
|
+
if (!isRecord$1(project) || typeof project.id !== "string" || typeof project.name !== "string" || typeof project.region !== "string") throw new SupabaseCliResponseError("Created Supabase project was not found.");
|
|
6766
|
+
return {
|
|
6767
|
+
id: project.id,
|
|
6768
|
+
name: project.name,
|
|
6769
|
+
region: project.region
|
|
6770
|
+
};
|
|
6771
|
+
},
|
|
6772
|
+
getProjectStatus: async (projectId) => {
|
|
6773
|
+
const body = await runSupabaseCli([
|
|
6774
|
+
"projects",
|
|
6775
|
+
"list",
|
|
6776
|
+
"--output",
|
|
6777
|
+
"json"
|
|
6778
|
+
]);
|
|
6779
|
+
if (!Array.isArray(body)) throw new SupabaseCliResponseError("Supabase projects response was invalid.");
|
|
6780
|
+
const project = body.find((candidate) => isRecord$1(candidate) && (candidate.id === projectId || candidate.ref === projectId));
|
|
6781
|
+
if (!isRecord$1(project) || typeof project.status !== "string") throw new SupabaseCliResponseError("Supabase project status was not found.");
|
|
6782
|
+
return project.status;
|
|
6783
|
+
}
|
|
6784
|
+
});
|
|
6785
|
+
//#endregion
|
|
6786
|
+
//#region iac/supabaseManagementApi.ts
|
|
6787
|
+
const SUPABASE_MANAGEMENT_API_URL = "https://api.supabase.com/v1";
|
|
6788
|
+
const SUPABASE_MANAGEMENT_API_TIMEOUT_MS = 3e4;
|
|
6789
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
6790
|
+
var SupabaseManagementApiStatusError = class extends Error {};
|
|
6791
|
+
const projectCreationMayHaveSucceededError = (message = "Supabase project creation could not be confirmed.", cause) => new Error(`${message} The request may have succeeded; check the organization's projects before retrying init.`, { cause });
|
|
6792
|
+
const request = async (accessToken, path, init) => {
|
|
6793
|
+
const method = init?.method ?? "GET";
|
|
6794
|
+
const controller = new AbortController();
|
|
6795
|
+
const timeout = setTimeout(() => controller.abort(), SUPABASE_MANAGEMENT_API_TIMEOUT_MS);
|
|
6796
|
+
timeout.unref();
|
|
6797
|
+
try {
|
|
6798
|
+
const response = await fetch(`${SUPABASE_MANAGEMENT_API_URL}${path}`, {
|
|
6799
|
+
body: init?.body ? JSON.stringify(init.body) : void 0,
|
|
6800
|
+
headers: {
|
|
6801
|
+
Authorization: `Bearer ${accessToken}`,
|
|
6802
|
+
"Content-Type": "application/json"
|
|
6803
|
+
},
|
|
6804
|
+
method,
|
|
6805
|
+
signal: controller.signal
|
|
6806
|
+
});
|
|
6807
|
+
if (!response.ok) throw new SupabaseManagementApiStatusError(`Supabase Management API request failed with status ${response.status}.`);
|
|
6808
|
+
return await response.json();
|
|
6809
|
+
} catch (error) {
|
|
6810
|
+
if (!controller.signal.aborted) {
|
|
6811
|
+
if (method === "POST" && !(error instanceof SupabaseManagementApiStatusError)) throw projectCreationMayHaveSucceededError(void 0, error);
|
|
6812
|
+
throw error;
|
|
6813
|
+
}
|
|
6814
|
+
if (method === "POST") throw projectCreationMayHaveSucceededError("Supabase project creation timed out.", error);
|
|
6815
|
+
throw new Error("Supabase Management API request timed out.");
|
|
6816
|
+
} finally {
|
|
6817
|
+
clearTimeout(timeout);
|
|
6818
|
+
}
|
|
6819
|
+
};
|
|
6820
|
+
const supabaseManagementApi = (accessToken) => accessToken === void 0 ? supabaseCliManagementApi() : {
|
|
6821
|
+
listOrganizations: async () => {
|
|
6822
|
+
const body = await request(accessToken, "/organizations");
|
|
6823
|
+
if (!Array.isArray(body)) throw new Error("Supabase organizations response was invalid.");
|
|
6824
|
+
return body.flatMap((organization) => isRecord(organization) && typeof organization.id === "string" && typeof organization.name === "string" && typeof organization.slug === "string" ? [{
|
|
6825
|
+
id: organization.id,
|
|
6826
|
+
name: organization.name,
|
|
6827
|
+
slug: organization.slug
|
|
6828
|
+
}] : []);
|
|
6829
|
+
},
|
|
6830
|
+
createProject: async ({ databasePassword, name, organizationSlug, region }) => {
|
|
6831
|
+
const body = await request(accessToken, "/projects", {
|
|
6832
|
+
body: {
|
|
6833
|
+
db_pass: databasePassword,
|
|
6834
|
+
name,
|
|
6835
|
+
organization_slug: organizationSlug,
|
|
6836
|
+
region
|
|
6837
|
+
},
|
|
6838
|
+
method: "POST"
|
|
6839
|
+
});
|
|
6840
|
+
if (!isRecord(body) || typeof body.ref !== "string" || typeof body.name !== "string" || typeof body.region !== "string") throw projectCreationMayHaveSucceededError("Supabase project creation response was invalid.");
|
|
6841
|
+
return {
|
|
6842
|
+
id: body.ref,
|
|
6843
|
+
name: body.name,
|
|
6844
|
+
region: body.region
|
|
6845
|
+
};
|
|
6846
|
+
},
|
|
6847
|
+
getProjectStatus: async (projectId) => {
|
|
6848
|
+
const body = await request(accessToken, `/projects/${encodeURIComponent(projectId)}`);
|
|
6849
|
+
if (!isRecord(body) || typeof body.status !== "string") throw new Error("Supabase project response was invalid.");
|
|
6850
|
+
return body.status;
|
|
6851
|
+
}
|
|
6852
|
+
};
|
|
6853
|
+
//#endregion
|
|
6373
6854
|
//#region iac/index.ts
|
|
6374
6855
|
const require$1 = createRequire(import.meta.url);
|
|
6375
6856
|
const EDGE_VENDOR_DIR = "_hot-updater";
|
|
6376
6857
|
const WORKSPACE_PACKAGE_PREFIX = "@hot-updater/";
|
|
6858
|
+
const SUPABASE_PROJECT_READY_STATUS = "ACTIVE_HEALTHY";
|
|
6859
|
+
const SUPABASE_PROJECT_PROVISIONING_STATUS = "COMING_UP";
|
|
6860
|
+
const SUPABASE_PROJECT_READINESS_MAX_ATTEMPTS = 300;
|
|
6861
|
+
const SUPABASE_PROJECT_READINESS_POLL_INTERVAL_MS = 1e3;
|
|
6377
6862
|
const STATIC_IMPORT_SPECIFIER_PATTERN = /^\s*(?:import|export)\s+(?:type\s+)?(?:[^"'`]+?\s+from\s+)?["']([^"']+)["'];?/gm;
|
|
6378
6863
|
const DYNAMIC_IMPORT_SPECIFIER_PATTERN = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
6379
6864
|
const getConfigScaffold = (build) => {
|
|
@@ -6551,7 +7036,7 @@ const buildEdgeFunctionImports = async (targetDir) => {
|
|
|
6551
7036
|
const resolveEdgeFunctionDenoConfig = async (targetDir) => {
|
|
6552
7037
|
return { imports: await buildEdgeFunctionImports(targetDir) };
|
|
6553
7038
|
};
|
|
6554
|
-
const selectProject = async () => {
|
|
7039
|
+
const selectProject = async (preferredProjectId, nonInteractive = false, accessToken) => {
|
|
6555
7040
|
const spinner = p.spinner();
|
|
6556
7041
|
spinner.start("Fetching Supabase projects...");
|
|
6557
7042
|
let projectsProcess = [];
|
|
@@ -6563,16 +7048,28 @@ const selectProject = async () => {
|
|
|
6563
7048
|
"list",
|
|
6564
7049
|
"--output",
|
|
6565
7050
|
"json"
|
|
6566
|
-
], {});
|
|
7051
|
+
], { env: accessToken ? { [initProvider.inputs.accessToken.envKey]: accessToken } : void 0 });
|
|
6567
7052
|
projectsProcess = listProjects.stdout === "null" ? [] : JSON.parse(listProjects?.stdout ?? "[]");
|
|
6568
7053
|
} catch (err) {
|
|
6569
7054
|
spinner.stop();
|
|
6570
|
-
|
|
7055
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7056
|
+
console.error(`Failed to fetch Supabase projects: ${message}`);
|
|
6571
7057
|
process.exit(1);
|
|
6572
7058
|
}
|
|
6573
7059
|
spinner.stop();
|
|
7060
|
+
const preferredProject = projectsProcess.find((project) => project.id === preferredProjectId);
|
|
7061
|
+
if (nonInteractive && preferredProject) {
|
|
7062
|
+
p.log.info(`Using saved Supabase project: ${preferredProject.name}`);
|
|
7063
|
+
return {
|
|
7064
|
+
create: false,
|
|
7065
|
+
project: preferredProject
|
|
7066
|
+
};
|
|
7067
|
+
}
|
|
7068
|
+
if (preferredProjectId && !preferredProject) p.log.warn("Saved Supabase project was not found. Select a project again.");
|
|
7069
|
+
if (nonInteractive) throw new MissingInitInputsError(["HOT_UPDATER_SUPABASE_PROJECT_ID"]);
|
|
6574
7070
|
const createProjectOption = `create/${Math.random().toString(36).substring(2, 15)}`;
|
|
6575
7071
|
const selectedProjectId = await p.select({
|
|
7072
|
+
initialValue: preferredProject?.id ?? projectsProcess[0]?.id,
|
|
6576
7073
|
message: "Select a Supabase project",
|
|
6577
7074
|
options: [...projectsProcess.map((project) => ({
|
|
6578
7075
|
label: `${project.name} (${project.region})`,
|
|
@@ -6583,29 +7080,15 @@ const selectProject = async () => {
|
|
|
6583
7080
|
}]
|
|
6584
7081
|
});
|
|
6585
7082
|
if (p.isCancel(selectedProjectId)) process.exit(0);
|
|
6586
|
-
if (selectedProjectId === createProjectOption) {
|
|
6587
|
-
try {
|
|
6588
|
-
await execa("npx", [
|
|
6589
|
-
"-y",
|
|
6590
|
-
"supabase",
|
|
6591
|
-
"projects",
|
|
6592
|
-
"create"
|
|
6593
|
-
], {
|
|
6594
|
-
stdio: "inherit",
|
|
6595
|
-
shell: true
|
|
6596
|
-
});
|
|
6597
|
-
} catch (err) {
|
|
6598
|
-
if (err instanceof ExecaError) console.error(err.stderr);
|
|
6599
|
-
else console.error(err);
|
|
6600
|
-
process.exit(1);
|
|
6601
|
-
}
|
|
6602
|
-
return selectProject();
|
|
6603
|
-
}
|
|
7083
|
+
if (selectedProjectId === createProjectOption) return { create: true };
|
|
6604
7084
|
const selectedProject = projectsProcess.find((project) => project.id === selectedProjectId);
|
|
6605
7085
|
if (!selectedProject) throw new Error("Project not found");
|
|
6606
|
-
return
|
|
7086
|
+
return {
|
|
7087
|
+
create: false,
|
|
7088
|
+
project: selectedProject
|
|
7089
|
+
};
|
|
6607
7090
|
};
|
|
6608
|
-
const selectBucket = async (api) => {
|
|
7091
|
+
const selectBucket = async (api, preferredBucketName, nonInteractive = false) => {
|
|
6609
7092
|
let buckets = [];
|
|
6610
7093
|
let retryCount = 0;
|
|
6611
7094
|
await p.tasks([{
|
|
@@ -6623,15 +7106,31 @@ const selectBucket = async (api) => {
|
|
|
6623
7106
|
process.exit(1);
|
|
6624
7107
|
}
|
|
6625
7108
|
}]);
|
|
7109
|
+
const preferredBucket = buckets.find((bucket) => bucket.name === preferredBucketName);
|
|
7110
|
+
if (nonInteractive && preferredBucket) {
|
|
7111
|
+
p.log.info(`Using saved Supabase bucket: ${preferredBucket.name}`);
|
|
7112
|
+
return {
|
|
7113
|
+
create: false,
|
|
7114
|
+
id: preferredBucket.id,
|
|
7115
|
+
isPublic: preferredBucket.isPublic,
|
|
7116
|
+
name: preferredBucket.name
|
|
7117
|
+
};
|
|
7118
|
+
}
|
|
7119
|
+
if (preferredBucketName && !preferredBucket) {
|
|
7120
|
+
if (nonInteractive) return {
|
|
7121
|
+
create: true,
|
|
7122
|
+
name: preferredBucketName
|
|
7123
|
+
};
|
|
7124
|
+
p.log.warn("Saved Supabase bucket was not found. Select a bucket again.");
|
|
7125
|
+
}
|
|
7126
|
+
if (nonInteractive) throw new MissingInitInputsError(["HOT_UPDATER_SUPABASE_BUCKET_NAME"]);
|
|
6626
7127
|
const createBucketOption = `create/${Math.random().toString(36).substring(2, 15)}`;
|
|
6627
7128
|
const selectedBucketId = await p.select({
|
|
7129
|
+
initialValue: preferredBucket?.id ?? buckets[0]?.id,
|
|
6628
7130
|
message: "Select a storage bucket",
|
|
6629
7131
|
options: [...buckets.map((bucket) => ({
|
|
6630
7132
|
label: bucket.name,
|
|
6631
|
-
value:
|
|
6632
|
-
id: bucket.id,
|
|
6633
|
-
name: bucket.name
|
|
6634
|
-
})
|
|
7133
|
+
value: bucket.id
|
|
6635
7134
|
})), {
|
|
6636
7135
|
label: "Create a new private bucket",
|
|
6637
7136
|
value: createBucketOption
|
|
@@ -6639,34 +7138,44 @@ const selectBucket = async (api) => {
|
|
|
6639
7138
|
});
|
|
6640
7139
|
if (p.isCancel(selectedBucketId)) process.exit(0);
|
|
6641
7140
|
if (selectedBucketId === createBucketOption) {
|
|
6642
|
-
const
|
|
7141
|
+
const prompt = initProvider.inputs.bucketName.prompt;
|
|
7142
|
+
const bucketName = await p.text({
|
|
7143
|
+
...getInitProviderTextPromptValues(prompt, preferredBucketName),
|
|
7144
|
+
message: prompt.message
|
|
7145
|
+
});
|
|
6643
7146
|
if (p.isCancel(bucketName)) process.exit(0);
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
if (!newBucket) throw new Error("Failed to create and select new bucket");
|
|
6649
|
-
return {
|
|
6650
|
-
id: newBucket.id,
|
|
6651
|
-
name: newBucket.name
|
|
6652
|
-
};
|
|
6653
|
-
} catch (err) {
|
|
6654
|
-
p.log.error(`Failed to create new bucket: ${err}`);
|
|
6655
|
-
process.exit(1);
|
|
6656
|
-
}
|
|
7147
|
+
return {
|
|
7148
|
+
create: true,
|
|
7149
|
+
name: bucketName
|
|
7150
|
+
};
|
|
6657
7151
|
}
|
|
6658
|
-
|
|
7152
|
+
const selectedBucket = buckets.find((bucket) => bucket.id === selectedBucketId);
|
|
7153
|
+
if (!selectedBucket) throw new Error("Selected Supabase bucket was not found.");
|
|
7154
|
+
return {
|
|
7155
|
+
create: false,
|
|
7156
|
+
id: selectedBucket.id,
|
|
7157
|
+
isPublic: selectedBucket.isPublic,
|
|
7158
|
+
name: selectedBucket.name
|
|
7159
|
+
};
|
|
6659
7160
|
};
|
|
6660
|
-
const
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
7161
|
+
const createSelectedBucket = async (api, selection) => {
|
|
7162
|
+
if (!selection.create) return selection;
|
|
7163
|
+
await api.createBucket(selection.name, { public: false });
|
|
7164
|
+
p.log.success(`Bucket "${selection.name}" created successfully.`);
|
|
7165
|
+
const bucket = (await api.listBuckets()).find((item) => item.name === selection.name);
|
|
7166
|
+
if (!bucket) throw new Error("Failed to create and select new bucket");
|
|
7167
|
+
return {
|
|
7168
|
+
id: bucket.id,
|
|
7169
|
+
name: bucket.name
|
|
7170
|
+
};
|
|
7171
|
+
};
|
|
7172
|
+
const deployEdgeFunction = async (accessToken, workdir, projectId, functionName) => {
|
|
6667
7173
|
const edgeFunctionsLibPath = path.join(workdir, "supabase", "edge-functions");
|
|
6668
7174
|
const edgeFunctionsCode = transformEnv(path.join(edgeFunctionsLibPath, "index.ts"), { FUNCTION_NAME: functionName });
|
|
6669
|
-
|
|
7175
|
+
if (!isSupabaseFunctionName(functionName)) throw new Error("Invalid Supabase Edge Function name.");
|
|
7176
|
+
const functionsDir = path.resolve(workdir, "supabase", "functions");
|
|
7177
|
+
const targetDir = path.resolve(functionsDir, functionName);
|
|
7178
|
+
if (!targetDir.startsWith(`${functionsDir}${path.sep}`)) throw new Error("Supabase Edge Function path escaped its output directory.");
|
|
6670
7179
|
await fs.mkdir(targetDir, { recursive: true });
|
|
6671
7180
|
const denoConfig = await resolveEdgeFunctionDenoConfig(targetDir);
|
|
6672
7181
|
const targetPath = path.join(targetDir, "index.ts");
|
|
@@ -6686,21 +7195,31 @@ const deployEdgeFunction = async (workdir, projectId) => {
|
|
|
6686
7195
|
"--no-verify-jwt",
|
|
6687
7196
|
"--workdir",
|
|
6688
7197
|
workdir
|
|
6689
|
-
], {
|
|
7198
|
+
], {
|
|
7199
|
+
cwd: workdir,
|
|
7200
|
+
env: getSupabaseCliEnv(accessToken)
|
|
7201
|
+
})).stdout;
|
|
6690
7202
|
} catch (err) {
|
|
6691
7203
|
if (err instanceof ExecaError && err.stderr) p.log.error(err.stderr);
|
|
6692
|
-
else
|
|
7204
|
+
else if (err instanceof Error) p.log.error(err.message);
|
|
6693
7205
|
process.exit(1);
|
|
6694
7206
|
}
|
|
6695
7207
|
}
|
|
6696
7208
|
}]);
|
|
6697
7209
|
};
|
|
6698
|
-
const
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
7210
|
+
const waitForSupabaseProjectReady = async ({ getProjectStatus, maxAttempts = SUPABASE_PROJECT_READINESS_MAX_ATTEMPTS, onLongWait, pollIntervalMs = SUPABASE_PROJECT_READINESS_POLL_INTERVAL_MS }) => {
|
|
7211
|
+
let lastStatus;
|
|
7212
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
7213
|
+
lastStatus = await getProjectStatus();
|
|
7214
|
+
if (lastStatus === SUPABASE_PROJECT_READY_STATUS) return;
|
|
7215
|
+
if (lastStatus !== SUPABASE_PROJECT_PROVISIONING_STATUS) throw new Error(`Supabase project entered unexpected status: ${lastStatus}.`);
|
|
7216
|
+
if (attempt === 5) onLongWait();
|
|
7217
|
+
if (attempt < maxAttempts - 1) await delay(pollIntervalMs);
|
|
7218
|
+
}
|
|
7219
|
+
throw new Error(`Timed out while waiting for the Supabase project. Last status: ${lastStatus ?? "unknown"}.`);
|
|
7220
|
+
};
|
|
7221
|
+
const getSupabaseProjectAccess = async ({ accessToken, managementApi, project, waitForProject }) => {
|
|
7222
|
+
const getServiceRoleApiKey = async () => {
|
|
6704
7223
|
const keysProcess = await execa("npx", [
|
|
6705
7224
|
"-y",
|
|
6706
7225
|
"supabase",
|
|
@@ -6710,17 +7229,146 @@ const runInit = async ({ build }) => {
|
|
|
6710
7229
|
project.id,
|
|
6711
7230
|
"--output",
|
|
6712
7231
|
"json"
|
|
6713
|
-
]);
|
|
6714
|
-
|
|
6715
|
-
}
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
7232
|
+
], { env: getSupabaseCliEnv(accessToken) });
|
|
7233
|
+
return JSON.parse(keysProcess.stdout ?? "[]").find((key) => key.name === "service_role")?.api_key;
|
|
7234
|
+
};
|
|
7235
|
+
let serviceRoleApiKey;
|
|
7236
|
+
if (waitForProject) await p.tasks([{
|
|
7237
|
+
title: `Waiting for ${project.name} to become ready...`,
|
|
7238
|
+
task: async (message) => {
|
|
7239
|
+
await waitForSupabaseProjectReady({
|
|
7240
|
+
getProjectStatus: () => managementApi.getProjectStatus(project.id),
|
|
7241
|
+
onLongWait: () => {
|
|
7242
|
+
message("Supabase project is still provisioning. This might take a few minutes.");
|
|
7243
|
+
}
|
|
7244
|
+
});
|
|
7245
|
+
serviceRoleApiKey = await getServiceRoleApiKey();
|
|
7246
|
+
return "Supabase project is ready.";
|
|
7247
|
+
}
|
|
7248
|
+
}]);
|
|
7249
|
+
else {
|
|
7250
|
+
const spinner = p.spinner();
|
|
7251
|
+
spinner.start(`Getting API keys for ${project.name}...`);
|
|
7252
|
+
try {
|
|
7253
|
+
serviceRoleApiKey = await getServiceRoleApiKey();
|
|
7254
|
+
} catch (error) {
|
|
7255
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7256
|
+
throw new Error(`Failed to get Supabase API keys: ${message}`);
|
|
7257
|
+
} finally {
|
|
7258
|
+
spinner.stop();
|
|
7259
|
+
}
|
|
6719
7260
|
}
|
|
6720
|
-
spinner.stop();
|
|
6721
|
-
const serviceRoleApiKey = apiKeys.find((key) => key.name === "service_role");
|
|
6722
7261
|
if (!serviceRoleApiKey) throw new Error("Service role key not found, is your project paused?");
|
|
6723
|
-
|
|
7262
|
+
return {
|
|
7263
|
+
api: supabaseApi(`https://${project.id}.supabase.co`, serviceRoleApiKey),
|
|
7264
|
+
serviceRoleApiKey
|
|
7265
|
+
};
|
|
7266
|
+
};
|
|
7267
|
+
const runInit = async ({ build, envFile }) => {
|
|
7268
|
+
const nonInteractive = envFile !== void 0;
|
|
7269
|
+
const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile);
|
|
7270
|
+
const { inputEnv, managedEnv } = initEnvSources;
|
|
7271
|
+
const savedInputs = resolveSupabaseInitInputs(getHotUpdaterInitInputEnv(initEnvSources, nonInteractive), {
|
|
7272
|
+
inputEnv,
|
|
7273
|
+
managedEnv
|
|
7274
|
+
});
|
|
7275
|
+
await assertSupabaseNonInteractiveInputs(savedInputs, nonInteractive);
|
|
7276
|
+
const initInputs = await inputSupabaseDeploymentInputs({
|
|
7277
|
+
...savedInputs,
|
|
7278
|
+
nonInteractive
|
|
7279
|
+
});
|
|
7280
|
+
const { accessToken, functionName } = initInputs;
|
|
7281
|
+
const projectSelection = await selectProject(savedInputs.projectId, nonInteractive, accessToken);
|
|
7282
|
+
let project = projectSelection.create ? void 0 : projectSelection.project;
|
|
7283
|
+
const dbPassword = await inputSupabaseDatabasePassword({
|
|
7284
|
+
cliHandlesPrompt: projectSelection.create && initInputs.accessToken === void 0,
|
|
7285
|
+
databasePassword: savedInputs.databasePassword,
|
|
7286
|
+
nonInteractive,
|
|
7287
|
+
required: projectSelection.create
|
|
7288
|
+
});
|
|
7289
|
+
const managementApi = supabaseManagementApi(accessToken);
|
|
7290
|
+
const projectCreationInputs = projectSelection.create ? await inputSupabaseProjectCreationInputs({
|
|
7291
|
+
bucketName: savedInputs.bucketName,
|
|
7292
|
+
organizationSlug: savedInputs.organizationSlug,
|
|
7293
|
+
organizations: await managementApi.listOrganizations(),
|
|
7294
|
+
projectName: savedInputs.projectName,
|
|
7295
|
+
region: savedInputs.region
|
|
7296
|
+
}) : void 0;
|
|
7297
|
+
let projectAccess = project === void 0 ? void 0 : await getSupabaseProjectAccess({
|
|
7298
|
+
accessToken,
|
|
7299
|
+
managementApi,
|
|
7300
|
+
project,
|
|
7301
|
+
waitForProject: false
|
|
7302
|
+
});
|
|
7303
|
+
let bucketSelection;
|
|
7304
|
+
if (project && projectAccess) bucketSelection = await selectBucket(projectAccess.api, savedInputs.bucketName, nonInteractive);
|
|
7305
|
+
else if (projectCreationInputs) bucketSelection = {
|
|
7306
|
+
create: true,
|
|
7307
|
+
name: projectCreationInputs.bucketName
|
|
7308
|
+
};
|
|
7309
|
+
else throw new Error("Failed to plan the Supabase storage bucket.");
|
|
7310
|
+
if (projectAccess) await ensureSupabaseBucketPrivate({
|
|
7311
|
+
api: projectAccess.api,
|
|
7312
|
+
nonInteractive,
|
|
7313
|
+
selection: bucketSelection
|
|
7314
|
+
});
|
|
7315
|
+
const inputsBeforeProvisioning = {
|
|
7316
|
+
...savedInputs,
|
|
7317
|
+
...projectCreationInputs,
|
|
7318
|
+
accessToken,
|
|
7319
|
+
bucketName: bucketSelection.name,
|
|
7320
|
+
databasePassword: dbPassword,
|
|
7321
|
+
functionName,
|
|
7322
|
+
projectId: project?.id
|
|
7323
|
+
};
|
|
7324
|
+
const databasePasswordKey = initProvider.inputs.databasePassword.envKey;
|
|
7325
|
+
const persistCredentialInputs = await confirmInitInputPersistence({
|
|
7326
|
+
existingEnv: dbPassword !== "" && (projectSelection.create || managedEnv["HOT_UPDATER_SUPABASE_DB_PASSWORD_PROJECT_ID"] !== project?.id) ? {
|
|
7327
|
+
...managedEnv,
|
|
7328
|
+
[databasePasswordKey]: ""
|
|
7329
|
+
} : managedEnv,
|
|
7330
|
+
inputs: inputsBeforeProvisioning,
|
|
7331
|
+
nonInteractive,
|
|
7332
|
+
provider: initProvider
|
|
7333
|
+
});
|
|
7334
|
+
if (!await confirmSupabaseDatabaseMigrations({ nonInteractive })) {
|
|
7335
|
+
p.log.info("Init cancelled.");
|
|
7336
|
+
process.exit(1);
|
|
7337
|
+
}
|
|
7338
|
+
if (projectSelection.create) {
|
|
7339
|
+
if (!projectCreationInputs) throw new Error("Supabase project creation inputs were not resolved.");
|
|
7340
|
+
project = await managementApi.createProject({
|
|
7341
|
+
databasePassword: dbPassword,
|
|
7342
|
+
name: projectCreationInputs.projectName,
|
|
7343
|
+
organizationSlug: projectCreationInputs.organizationSlug,
|
|
7344
|
+
region: projectCreationInputs.region
|
|
7345
|
+
});
|
|
7346
|
+
projectAccess = await getSupabaseProjectAccess({
|
|
7347
|
+
accessToken,
|
|
7348
|
+
managementApi,
|
|
7349
|
+
project,
|
|
7350
|
+
waitForProject: true
|
|
7351
|
+
});
|
|
7352
|
+
}
|
|
7353
|
+
if (!project || !projectAccess) throw new Error("Failed to resolve the Supabase project.");
|
|
7354
|
+
const providerEnv = getInitProviderEnvVars({
|
|
7355
|
+
includeConsentInputs: persistCredentialInputs,
|
|
7356
|
+
inputs: {
|
|
7357
|
+
...inputsBeforeProvisioning,
|
|
7358
|
+
projectId: project.id
|
|
7359
|
+
},
|
|
7360
|
+
provider: initProvider
|
|
7361
|
+
});
|
|
7362
|
+
const persistDatabasePassword = persistCredentialInputs && dbPassword !== "";
|
|
7363
|
+
if (persistDatabasePassword) providerEnv[SUPABASE_DATABASE_PASSWORD_PROJECT_ID_ENV_KEY] = project.id;
|
|
7364
|
+
await makeEnv(providerEnv, ".env.hotupdater", { removeKeys: persistDatabasePassword ? [] : [databasePasswordKey, SUPABASE_DATABASE_PASSWORD_PROJECT_ID_ENV_KEY] });
|
|
7365
|
+
const bucket = await createSelectedBucket(projectAccess.api, bucketSelection);
|
|
7366
|
+
await makeEnv({
|
|
7367
|
+
[initProvider.inputs.projectId.envKey]: project.id,
|
|
7368
|
+
HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY: projectAccess.serviceRoleApiKey,
|
|
7369
|
+
[initProvider.inputs.bucketName.envKey]: bucket.name,
|
|
7370
|
+
HOT_UPDATER_SUPABASE_URL: `https://${project.id}.supabase.co`
|
|
7371
|
+
});
|
|
6724
7372
|
const { tmpDir, removeTmpDir } = await copyDirToTmp(path.dirname(path.resolve(require$1.resolve("@hot-updater/supabase/scaffold"))), "supabase");
|
|
6725
7373
|
const migrationPath = await path.join(tmpDir, "supabase", "migrations");
|
|
6726
7374
|
const migrationFiles = await fs.readdir(migrationPath);
|
|
@@ -6729,29 +7377,26 @@ const runInit = async ({ build }) => {
|
|
|
6729
7377
|
const content = await fs.readFile(filePath, "utf-8");
|
|
6730
7378
|
await fs.writeFile(filePath, transformTemplate(content, { BUCKET_NAME: bucket.name }));
|
|
6731
7379
|
}
|
|
6732
|
-
const dbPassword = await p.password({ message: "Enter your Supabase database password (press Enter to skip if none)" });
|
|
6733
|
-
if (p.isCancel(dbPassword)) process.exit(0);
|
|
6734
7380
|
await linkSupabase(tmpDir, {
|
|
7381
|
+
accessToken,
|
|
6735
7382
|
projectId: project.id,
|
|
6736
7383
|
dbPassword
|
|
6737
7384
|
});
|
|
6738
|
-
await pushDB(tmpDir, {
|
|
6739
|
-
|
|
7385
|
+
await pushDB(tmpDir, {
|
|
7386
|
+
accessToken,
|
|
7387
|
+
dbPassword
|
|
7388
|
+
});
|
|
7389
|
+
await deployEdgeFunction(accessToken, tmpDir, project.id, functionName);
|
|
6740
7390
|
await removeTmpDir();
|
|
6741
7391
|
const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build));
|
|
6742
7392
|
await assertSkippedConfigDoesNotUseLegacySupabaseKey(configWriteResult);
|
|
6743
|
-
await makeEnv({
|
|
6744
|
-
HOT_UPDATER_SUPABASE_SERVICE_ROLE_KEY: serviceRoleApiKey.api_key,
|
|
6745
|
-
HOT_UPDATER_SUPABASE_BUCKET_NAME: bucket.name,
|
|
6746
|
-
HOT_UPDATER_SUPABASE_URL: `https://${project.id}.supabase.co`
|
|
6747
|
-
});
|
|
6748
7393
|
p.log.success("Generated '.env.hotupdater' file with Supabase settings.");
|
|
6749
7394
|
if (configWriteResult.status === "created") p.log.success("Generated 'hot-updater.config.ts' file with Supabase settings.");
|
|
6750
7395
|
else if (configWriteResult.status === "merged") p.log.success("Updated 'hot-updater.config.ts' file with Supabase settings.");
|
|
6751
7396
|
else p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
|
|
6752
|
-
p.note(transformTemplate(SOURCE_TEMPLATE, { source: `https://${project.id}.supabase.co/functions/v1
|
|
7397
|
+
p.note(transformTemplate(SOURCE_TEMPLATE, { source: `https://${project.id}.supabase.co/functions/v1/${functionName}` }));
|
|
6753
7398
|
p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/supabase#step-4-add-hotupdater-to-your-project")}`);
|
|
6754
7399
|
p.log.success("Done! 🎉");
|
|
6755
7400
|
};
|
|
6756
7401
|
//#endregion
|
|
6757
|
-
export { getLegacySupabaseConfigReference, resolveEdgeFunctionDenoConfig, runInit, selectBucket, selectProject };
|
|
7402
|
+
export { createSelectedBucket, getLegacySupabaseConfigReference, getSupabaseProjectAccess, resolveEdgeFunctionDenoConfig, runInit, selectBucket, selectProject, waitForSupabaseProjectReady };
|