convex
Version:
Client for the Convex Cloud
560 lines (559 loc) • 17.8 kB
JavaScript
;
import { logVerbose, logWarning } from "../../bundler/context.js";
import { getTeamAndProjectFromPreviewAdminKey } from "./deployment.js";
import {
assertLocalBackendRunning,
localDeploymentUrl
} from "./localDeployment/run.js";
import {
ThrowingFetchError,
bigBrainAPI,
bigBrainAPIMaybeThrows,
logAndHandleFetchError
} from "./utils/utils.js";
import { z } from "zod";
import { loadLocalDeploymentCredentials } from "./localDeployment/localDeployment.js";
import { loadAnonymousDeployment } from "./localDeployment/anonymous.js";
export async function createProject(ctx, {
teamSlug: selectedTeamSlug,
projectName,
partitionId,
deploymentTypeToProvision
}) {
const provisioningArgs = {
team: selectedTeamSlug,
projectName,
// TODO: Consider allowing projects with no deployments, or consider switching
// to provisioning prod on creation.
deploymentType: deploymentTypeToProvision,
partitionId
};
const data = await bigBrainAPI({
ctx,
method: "POST",
url: "create_project",
data: provisioningArgs
});
const { projectSlug, teamSlug, projectsRemaining } = data;
if (projectSlug === void 0 || teamSlug === void 0 || projectsRemaining === void 0) {
const error = "Unexpected response during provisioning: " + JSON.stringify(data);
return await ctx.crash({
exitCode: 1,
errorType: "transient",
errForSentry: error,
printedMessage: error
});
}
return {
projectSlug,
teamSlug,
projectsRemaining
};
}
export const deploymentSelectionWithinProjectSchema = z.discriminatedUnion(
"kind",
[
z.object({ kind: z.literal("previewName"), previewName: z.string() }),
z.object({ kind: z.literal("deploymentName"), deploymentName: z.string() }),
z.object({ kind: z.literal("prod"), partitionId: z.number().optional() }),
z.object({
kind: z.literal("implicitProd"),
partitionId: z.number().optional()
}),
z.object({ kind: z.literal("ownDev"), partitionId: z.number().optional() })
]
);
export async function deploymentSelectionWithinProjectFromOptions(ctx, options) {
if (options.previewName !== void 0) {
return { kind: "previewName", previewName: options.previewName };
}
if (options.deploymentName !== void 0) {
return { kind: "deploymentName", deploymentName: options.deploymentName };
}
const partitionId = options.partitionId ? parseInt(options.partitionId) : void 0;
if (options.prod) {
return { kind: "prod", partitionId };
}
if (options.implicitProd) {
return { kind: "implicitProd", partitionId };
}
return { kind: "ownDev", partitionId };
}
export async function validateDeploymentSelectionForExistingDeployment(ctx, deploymentSelection, source) {
if (deploymentSelection.kind === "ownDev" || deploymentSelection.kind === "implicitProd") {
return;
}
switch (source) {
case "selfHosted":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "The `--prod`, `--preview-name`, and `--deployment-name` flags cannot be used with a self-hosted deployment."
});
case "deployKey":
logWarning(
ctx,
"Ignoring `--prod`, `--preview-name`, or `--deployment-name` flags and using deployment from CONVEX_DEPLOY_KEY"
);
break;
case "cliArgs":
logWarning(
ctx,
"Ignoring `--prod`, `--preview-name`, or `--deployment-name` flags since this command was run with --url and --admin-key"
);
break;
}
}
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);
}
}
export async function checkAccessToSelectedProject(ctx, projectSelection) {
switch (projectSelection.kind) {
case "deploymentName": {
const result = await getTeamAndProjectSlugForDeployment(ctx, {
deploymentName: projectSelection.deploymentName
});
if (result === null) {
return { kind: "noAccess" };
}
return {
kind: "hasAccess",
teamSlug: result.teamSlug,
projectSlug: result.projectSlug
};
}
case "teamAndProjectSlugs": {
const hasAccess = await hasAccessToProject(ctx, {
teamSlug: projectSelection.teamSlug,
projectSlug: projectSelection.projectSlug
});
if (!hasAccess) {
return { kind: "noAccess" };
}
return {
kind: "hasAccess",
teamSlug: projectSelection.teamSlug,
projectSlug: projectSelection.projectSlug
};
}
case "projectDeployKey":
return { kind: "unknown" };
default: {
const _exhaustivenessCheck = projectSelection;
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid project selection: ${projectSelection.kind}`
});
}
}
}
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);
}
}
export async function fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(ctx, projectSelection, deploymentType, partitionId) {
if (projectSelection.kind === "projectDeployKey") {
const auth = ctx.bigBrainAuth();
const doesAuthMatch = auth !== null && auth.kind === "projectKey" && auth.projectKey === projectSelection.projectDeployKey;
if (!doesAuthMatch) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
errForSentry: new Error(
"Expected project deploy key to match the big brain auth header"
),
printedMessage: "Unexpected error when loading the Convex deployment"
});
}
}
let data;
try {
data = await bigBrainAPIMaybeThrows({
ctx,
method: "POST",
url: "deployment/provision_and_authorize",
data: {
teamSlug: projectSelection.kind === "teamAndProjectSlugs" ? projectSelection.teamSlug : null,
projectSlug: projectSelection.kind === "teamAndProjectSlugs" ? projectSelection.projectSlug : null,
deploymentType: deploymentType === "prod" ? "prod" : "dev",
partitionId
}
});
} catch (error) {
const msg = "Unknown error during authorization: " + error;
return await ctx.crash({
exitCode: 1,
errorType: "transient",
errForSentry: new Error(msg),
printedMessage: msg
});
}
const adminKey = data.adminKey;
const url = data.url;
const deploymentName = data.deploymentName;
if (adminKey === void 0 || url === void 0) {
const msg = "Unknown error during authorization: " + JSON.stringify(data);
return await ctx.crash({
exitCode: 1,
errorType: "transient",
errForSentry: new Error(msg),
printedMessage: msg
});
}
return { adminKey, deploymentUrl: url, deploymentName };
}
async function fetchExistingDevDeploymentCredentialsOrCrash(ctx, deploymentName) {
const slugs = await fetchTeamAndProject(ctx, deploymentName);
const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
ctx,
{
kind: "teamAndProjectSlugs",
teamSlug: slugs.team,
projectSlug: slugs.project
},
"dev",
void 0
);
return {
deploymentName: credentials.deploymentName,
adminKey: credentials.adminKey,
url: credentials.deploymentUrl,
deploymentType: "dev"
};
}
async function handleOwnDev(ctx, projectSelection, partitionId) {
switch (projectSelection.kind) {
case "deploymentName": {
if (projectSelection.deploymentType === "local") {
const credentials = await loadLocalDeploymentCredentials(
ctx,
projectSelection.deploymentName
);
return {
deploymentName: projectSelection.deploymentName,
adminKey: credentials.adminKey,
url: credentials.deploymentUrl,
deploymentType: "local"
};
}
return await fetchExistingDevDeploymentCredentialsOrCrash(
ctx,
projectSelection.deploymentName
);
}
case "teamAndProjectSlugs":
case "projectDeployKey": {
const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
ctx,
projectSelection,
"dev",
partitionId
);
return {
url: credentials.deploymentUrl,
adminKey: credentials.adminKey,
deploymentName: credentials.deploymentName,
deploymentType: "dev"
};
}
}
}
async function handleProd(ctx, projectSelection, partitionId) {
switch (projectSelection.kind) {
case "deploymentName": {
const credentials = await bigBrainAPI({
ctx,
method: "POST",
url: "deployment/authorize_prod",
data: {
deploymentName: projectSelection.deploymentName,
partitionId
}
});
return credentials;
}
case "teamAndProjectSlugs":
case "projectDeployKey": {
const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
ctx,
projectSelection,
"prod",
partitionId
);
return {
url: credentials.deploymentUrl,
adminKey: credentials.adminKey,
deploymentName: credentials.deploymentName,
deploymentType: "prod"
};
}
}
}
async function handlePreview(ctx, previewName, projectSelection) {
switch (projectSelection.kind) {
case "deploymentName":
case "teamAndProjectSlugs":
return await bigBrainAPI({
ctx,
method: "POST",
url: "deployment/authorize_preview",
data: {
previewName,
projectSelection
}
});
case "projectDeployKey":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Project deploy keys are not supported for preview deployments"
});
}
}
async function handleDeploymentName(ctx, deploymentName, projectSelection) {
switch (projectSelection.kind) {
case "deploymentName":
case "teamAndProjectSlugs":
return await bigBrainAPI({
ctx,
method: "POST",
url: "deployment/authorize_within_current_project",
data: {
selectedDeploymentName: deploymentName,
projectSelection
}
});
case "projectDeployKey":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Project deploy keys are not supported with the --deployment-name flag"
});
}
}
async function fetchDeploymentCredentialsWithinCurrentProject(ctx, projectSelection, deploymentSelection) {
switch (deploymentSelection.kind) {
case "ownDev": {
return await handleOwnDev(
ctx,
projectSelection,
deploymentSelection.partitionId
);
}
case "implicitProd":
case "prod": {
return await handleProd(
ctx,
projectSelection,
deploymentSelection.partitionId
);
}
case "previewName":
return await handlePreview(
ctx,
deploymentSelection.previewName,
projectSelection
);
case "deploymentName":
return await handleDeploymentName(
ctx,
deploymentSelection.deploymentName,
projectSelection
);
default: {
const _exhaustivenessCheck = deploymentSelection;
return ctx.crash({
exitCode: 1,
errorType: "fatal",
// This should be unreachable, so don't bother with a printed message.
printedMessage: null,
errForSentry: `Unexpected deployment selection: ${deploymentSelection}`
});
}
}
}
async function _loadExistingDeploymentCredentialsForProject(ctx, targetProject, deploymentSelection, { ensureLocalRunning } = { ensureLocalRunning: true }) {
const accessResult = await checkAccessToSelectedProject(ctx, targetProject);
if (accessResult.kind === "noAccess") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "You don't have access to the selected project. Run `npx convex dev` to select a different project."
});
}
const result = await fetchDeploymentCredentialsWithinCurrentProject(
ctx,
targetProject,
deploymentSelection
);
logVerbose(
ctx,
`Deployment URL: ${result.url}, Deployment Name: ${result.deploymentName}, Deployment Type: ${result.deploymentType}`
);
if (ensureLocalRunning && result.deploymentType === "local") {
await assertLocalBackendRunning(ctx, {
url: result.url,
deploymentName: result.deploymentName
});
}
return {
...result,
deploymentFields: {
deploymentName: result.deploymentName,
deploymentType: result.deploymentType,
projectSlug: accessResult.kind === "hasAccess" ? accessResult.projectSlug : null,
teamSlug: accessResult.kind === "hasAccess" ? accessResult.teamSlug : null
}
};
}
export async function loadSelectedDeploymentCredentials(ctx, deploymentSelection, selectionWithinProject, { ensureLocalRunning } = { ensureLocalRunning: true }) {
switch (deploymentSelection.kind) {
case "existingDeployment":
await validateDeploymentSelectionForExistingDeployment(
ctx,
selectionWithinProject,
deploymentSelection.deploymentToActOn.source
);
logVerbose(
ctx,
`Deployment URL: ${deploymentSelection.deploymentToActOn.url}, Deployment Name: ${deploymentSelection.deploymentToActOn.deploymentFields?.deploymentName ?? "unknown"}, Deployment Type: ${deploymentSelection.deploymentToActOn.deploymentFields?.deploymentType ?? "unknown"}`
);
return {
adminKey: deploymentSelection.deploymentToActOn.adminKey,
url: deploymentSelection.deploymentToActOn.url,
deploymentFields: deploymentSelection.deploymentToActOn.deploymentFields
};
case "chooseProject":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "No CONVEX_DEPLOYMENT set, run `npx convex dev` to configure a Convex project"
});
case "preview": {
const slugs = await getTeamAndProjectFromPreviewAdminKey(
ctx,
deploymentSelection.previewDeployKey
);
return await _loadExistingDeploymentCredentialsForProject(
ctx,
{
kind: "teamAndProjectSlugs",
teamSlug: slugs.teamSlug,
projectSlug: slugs.projectSlug
},
selectionWithinProject,
{ ensureLocalRunning }
);
}
case "deploymentWithinProject": {
return await _loadExistingDeploymentCredentialsForProject(
ctx,
deploymentSelection.targetProject,
selectionWithinProject,
{ ensureLocalRunning }
);
}
case "anonymous": {
if (deploymentSelection.deploymentName === null) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "No CONVEX_DEPLOYMENT set, run `npx convex dev` to configure a Convex project"
});
}
const config = await loadAnonymousDeployment(
ctx,
deploymentSelection.deploymentName
);
const url = localDeploymentUrl(config.ports.cloud);
if (ensureLocalRunning) {
await assertLocalBackendRunning(ctx, {
url,
deploymentName: deploymentSelection.deploymentName
});
}
return {
adminKey: config.adminKey,
url,
deploymentFields: {
deploymentName: deploymentSelection.deploymentName,
deploymentType: "anonymous",
projectSlug: null,
teamSlug: null
}
};
}
default: {
const _exhaustivenessCheck = deploymentSelection;
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Unknown deployment type"
});
}
}
}
export async function fetchTeamAndProject(ctx, deploymentName) {
const data = await bigBrainAPI({
ctx,
method: "GET",
url: `deployment/${deploymentName}/team_and_project`
});
const { team, project } = data;
if (team === void 0 || project === void 0) {
const msg = "Unknown error when fetching team and project: " + JSON.stringify(data);
return await ctx.crash({
exitCode: 1,
errorType: "transient",
errForSentry: new Error(msg),
printedMessage: msg
});
}
return data;
}
export async function fetchTeamAndProjectForKey(ctx, deployKey) {
const data = await bigBrainAPI({
ctx,
method: "POST",
url: `deployment/team_and_project_for_key`,
data: {
deployKey
}
});
const { team, project } = data;
if (team === void 0 || project === void 0) {
const msg = "Unknown error when fetching team and project: " + JSON.stringify(data);
return await ctx.crash({
exitCode: 1,
errorType: "transient",
errForSentry: new Error(msg),
printedMessage: msg
});
}
return data;
}
//# sourceMappingURL=api.js.map