convex
Version:
Client for the Convex Cloud
326 lines (325 loc) • 10.5 kB
JavaScript
;
import chalk from "chalk";
import {
logFailure,
logFinishedStep,
logMessage,
logWarning,
showSpinner
} from "../bundler/context.js";
import {
fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows,
createProject
} from "./lib/api.js";
import {
configFilepath,
configName,
readProjectConfig,
upgradeOldAuthInfoToAuthConfig,
writeProjectConfig
} from "./lib/config.js";
import {
CONVEX_DEPLOYMENT_VAR_NAME,
eraseDeploymentEnvVar,
writeDeploymentEnvVar
} from "./lib/deployment.js";
import { finalizeConfiguration } from "./lib/init.js";
import {
bigBrainAPIMaybeThrows,
functionsDir,
getConfiguredDeploymentName,
hasProjects,
logAndHandleFetchError,
ThrowingFetchError,
validateOrSelectProject,
validateOrSelectTeam
} from "./lib/utils/utils.js";
import { writeConvexUrlToEnvFile } from "./lib/envvars.js";
import path from "path";
import { projectDashboardUrl } from "./dashboard.js";
import { doCodegen, doInitCodegen } from "./lib/codegen.js";
import { handleLocalDeployment } from "./lib/localDeployment/localDeployment.js";
import { promptOptions, promptString } from "./lib/utils/prompts.js";
export async function deploymentCredentialsOrConfigure(ctx, chosenConfiguration, cmdOptions) {
if (cmdOptions.url !== void 0 && cmdOptions.adminKey !== void 0) {
const credentials = await handleManuallySetUrlAndAdminKey(ctx, {
url: cmdOptions.url,
adminKey: cmdOptions.adminKey
});
return { ...credentials, cleanupHandle: null };
}
const { projectSlug, teamSlug } = await selectProject(
ctx,
chosenConfiguration,
{ team: cmdOptions.team, project: cmdOptions.project }
);
const deploymentOptions = cmdOptions.prod ? { kind: "prod" } : cmdOptions.local ? { kind: "local", ...cmdOptions.localOptions } : { kind: "dev" };
const {
deploymentName,
deploymentUrl: url,
adminKey,
cleanupHandle
} = await ensureDeploymentProvisioned(ctx, {
teamSlug,
projectSlug,
deploymentOptions
});
await updateEnvAndConfigForDeploymentSelection(ctx, {
url,
deploymentName,
teamSlug,
projectSlug,
deploymentType: deploymentOptions.kind
});
return { deploymentName, url, adminKey, cleanupHandle };
}
async function handleManuallySetUrlAndAdminKey(ctx, cmdOptions) {
const { url, adminKey } = cmdOptions;
const didErase = await eraseDeploymentEnvVar(ctx);
if (didErase) {
logMessage(
ctx,
chalk.yellowBright(
`Removed the CONVEX_DEPLOYMENT environment variable from .env.local`
)
);
}
const envVarWrite = await writeConvexUrlToEnvFile(ctx, url);
if (envVarWrite !== null) {
logMessage(
ctx,
chalk.green(
`Saved the given --url as ${envVarWrite.envVar} to ${envVarWrite.envFile}`
)
);
}
return { url, adminKey };
}
async function selectProject(ctx, chosenConfiguration, cmdOptions) {
let result = null;
if (chosenConfiguration === null) {
result = await getConfiguredProjectSlugs(ctx);
if (result !== null && result !== "AccessDenied") {
return result;
}
}
const reconfigure = result === "AccessDenied";
const choice = chosenConfiguration !== "ask" && chosenConfiguration !== null ? chosenConfiguration : await askToConfigure(ctx, reconfigure);
switch (choice) {
case "new":
return selectNewProject(ctx, cmdOptions);
case "existing":
return selectExistingProject(ctx, cmdOptions);
default:
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "No project selected."
});
}
}
async function getConfiguredProjectSlugs(ctx) {
const deploymentName = await getConfiguredDeploymentName(ctx);
if (deploymentName !== null) {
const result = await getTeamAndProjectSlugForDeployment(ctx, {
deploymentName,
kind: "cloud"
});
if (result !== null) {
return result;
} else {
logFailure(
ctx,
`You don't have access to the project with deployment ${chalk.bold(
deploymentName
)}, as configured in ${chalk.bold(CONVEX_DEPLOYMENT_VAR_NAME)}`
);
return "AccessDenied";
}
}
const { projectConfig } = await readProjectConfig(ctx);
const { team, project } = projectConfig;
if (typeof team === "string" && typeof project === "string") {
const hasAccess = await hasAccessToProject(ctx, {
teamSlug: team,
projectSlug: project
});
if (!hasAccess) {
logFailure(
ctx,
`You don't have access to the project ${chalk.bold(project)} in team ${chalk.bold(team)} as configured in ${chalk.bold("convex.json")}`
);
return "AccessDenied";
}
return { teamSlug: team, projectSlug: project };
}
return null;
}
async function getTeamAndProjectSlugForDeployment(ctx, selector) {
try {
const body = await bigBrainAPIMaybeThrows({
ctx,
url: `/api/deployment/${selector.deploymentName}/team_and_project`,
method: "GET"
});
return { teamSlug: body.team, projectSlug: body.project };
} catch (err) {
if (err instanceof ThrowingFetchError && (err.serverErrorData?.code === "DeploymentNotFound" || err.serverErrorData?.code === "ProjectNotFound")) {
return null;
}
return logAndHandleFetchError(ctx, err);
}
}
async function hasAccessToProject(ctx, selector) {
try {
await bigBrainAPIMaybeThrows({
ctx,
url: `/api/teams/${selector.teamSlug}/projects/${selector.projectSlug}/deployments`,
method: "GET"
});
return true;
} catch (err) {
if (err instanceof ThrowingFetchError && (err.serverErrorData?.code === "TeamNotFound" || err.serverErrorData?.code === "ProjectNotFound")) {
return false;
}
return logAndHandleFetchError(ctx, err);
}
}
const cwd = path.basename(process.cwd());
async function selectNewProject(ctx, config) {
const { teamSlug: selectedTeam, chosen: didChooseBetweenTeams } = await validateOrSelectTeam(ctx, config.team, "Team:");
let projectName = config.project || cwd;
if (!config.project) {
projectName = await promptString(ctx, {
message: "Project name:",
default: cwd
});
}
showSpinner(ctx, "Creating new Convex project...");
let projectSlug, teamSlug, projectsRemaining;
try {
({ projectSlug, teamSlug, projectsRemaining } = await createProject(ctx, {
teamSlug: selectedTeam,
projectName
}));
} catch (err) {
logFailure(ctx, "Unable to create project.");
return await logAndHandleFetchError(ctx, err);
}
const teamMessage = didChooseBetweenTeams ? " in team " + chalk.bold(teamSlug) : "";
logFinishedStep(
ctx,
`Created project ${chalk.bold(
projectSlug
)}${teamMessage}, manage it at ${chalk.bold(
projectDashboardUrl(teamSlug, projectSlug)
)}`
);
if (projectsRemaining <= 2) {
logWarning(
ctx,
chalk.yellow.bold(
`Your account now has ${projectsRemaining} project${projectsRemaining === 1 ? "" : "s"} remaining.`
)
);
}
const { projectConfig: existingProjectConfig } = await readProjectConfig(ctx);
const configPath = await configFilepath(ctx);
const functionsPath = functionsDir(configPath, existingProjectConfig);
await doInitCodegen(ctx, functionsPath, true);
await doCodegen(ctx, functionsPath, "disable");
return { teamSlug, projectSlug };
}
async function selectExistingProject(ctx, config) {
const { teamSlug } = await validateOrSelectTeam(ctx, config.team, "Team:");
const projectSlug = await validateOrSelectProject(
ctx,
config.project,
teamSlug,
"Configure project",
"Project:"
);
if (projectSlug === null) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Run the command again to create a new project instead."
});
}
showSpinner(ctx, `Reinitializing project ${projectSlug}...
`);
const { projectConfig: existingProjectConfig } = await readProjectConfig(ctx);
const functionsPath = functionsDir(configName(), existingProjectConfig);
await doCodegen(ctx, functionsPath, "disable");
return { teamSlug, projectSlug };
}
async function askToConfigure(ctx, reconfigure) {
if (!await hasProjects(ctx)) {
return "new";
}
return await promptOptions(ctx, {
message: reconfigure ? "Configure a different project?" : "What would you like to configure?",
default: "new",
choices: [
{ name: "create a new project", value: "new" },
{ name: "choose an existing project", value: "existing" }
]
});
}
async function ensureDeploymentProvisioned(ctx, options) {
switch (options.deploymentOptions.kind) {
case "dev":
case "prod": {
const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
ctx,
{ teamSlug: options.teamSlug, projectSlug: options.projectSlug },
options.deploymentOptions.kind
);
return {
...credentials,
cleanupHandle: null,
onActivity: null
};
}
case "local": {
const credentials = await handleLocalDeployment(ctx, {
teamSlug: options.teamSlug,
projectSlug: options.projectSlug,
...options.deploymentOptions
});
return credentials;
}
default:
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid deployment type: ${options.deploymentOptions.kind}`,
errForSentry: `Invalid deployment type: ${options.deploymentOptions.kind}`
});
}
}
async function updateEnvAndConfigForDeploymentSelection(ctx, options) {
const { configPath, projectConfig: existingProjectConfig } = await readProjectConfig(ctx);
const functionsPath = functionsDir(configName(), existingProjectConfig);
const { wroteToGitIgnore, changedDeploymentEnvVar } = await writeDeploymentEnvVar(ctx, options.deploymentType, {
team: options.teamSlug,
project: options.projectSlug,
deploymentName: options.deploymentName
});
const projectConfig = await upgradeOldAuthInfoToAuthConfig(
ctx,
existingProjectConfig,
functionsPath
);
await writeProjectConfig(ctx, projectConfig, {
deleteIfAllDefault: true
});
await finalizeConfiguration(ctx, {
deploymentType: options.deploymentType,
url: options.url,
wroteToGitIgnore,
changedDeploymentEnvVar,
functionsPath: functionsDir(configPath, projectConfig)
});
}
//# sourceMappingURL=configure.js.map