UNPKG

convex

Version:

Client for the Convex Cloud

713 lines (710 loc) 24.9 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var deploymentCreate_exports = {}; __export(deploymentCreate_exports, { createLocalDeployment: () => createLocalDeployment, deploymentCreate: () => deploymentCreate, fetchAvailableClasses: () => fetchAvailableClasses, fetchAvailableRegions: () => fetchAvailableRegions, resolveClassDetails: () => resolveClassDetails, resolveRegionDetails: () => resolveRegionDetails }); module.exports = __toCommonJS(deploymentCreate_exports); var import_child_process = require("child_process"); var import_extra_typings = require("@commander-js/extra-typings"); var import_context = require("../bundler/context.js"); var import_log = require("../bundler/log.js"); var import_deploymentSelection = require("./lib/deploymentSelection.js"); var import_utils = require("./lib/utils/utils.js"); var import_deployment = require("./lib/deployment.js"); var import_deploymentSelect = require("./deploymentSelect.js"); var import_prompts = require("./lib/utils/prompts.js"); var import_chalk = require("chalk"); var import_deploymentSelector = require("./lib/deploymentSelector.js"); var import_expiration = require("./lib/expiration.js"); var import_download = require("./lib/localDeployment/download.js"); var import_filePaths = require("./lib/localDeployment/filePaths.js"); var import_utils2 = require("./lib/localDeployment/utils.js"); var import_bigBrain = require("./lib/localDeployment/bigBrain.js"); var import_localDeployment = require("./lib/localDeployment/localDeployment.js"); var import_run = require("./lib/localDeployment/run.js"); const SUPPORTED_TYPES = ["dev", "prod", "preview"]; const deploymentCreate = new import_extra_typings.Command("create").summary("Create a new deployment for a project").description( "Create a new deployment for a project.\n\n Create a dev deployment and select it: `npx convex deployment create dev/my-new-feature --type dev --select`\n Create a prod deployment named \u201Cstaging\u201D: `npx convex deployment create staging --type prod`\n Create a local deployment: `npx convex deployment create local`\n" ).argument("[ref]").allowExcessArguments(false).addOption( new import_extra_typings.Option("--type <type>", "Deployment type").choices(SUPPORTED_TYPES) ).option("--region <region>", "Deployment region").addOption(new import_extra_typings.Option("--class <class>", "Deployment class").hideHelp()).option( "--select", "Select the new deployment. This will update the Convex environment variables in .env.local. Subsequent `npx convex` commands will run against this deployment." ).option( "--default", "Make the new deployment your default production deployment (used by `npx convex deploy`) or your personal dev deployment." ).option( "--expiration <value>", 'When the deployment expires (e.g. "none", "in 7 days", "2026-04-01T00:00:00Z", or a UNIX timestamp in seconds or milliseconds)' ).addOption(new import_extra_typings.Option("--expiry <value>").hideHelp()).addOption(new import_extra_typings.Option("--expires <value>").hideHelp()).action(async (refParam, options) => { const expiration = options.expiration ?? options.expiry ?? options.expires; const ctx = await (0, import_context.oneoffContext)({ url: void 0, adminKey: void 0, envFile: void 0 }); const currentDeployment = await (0, import_deploymentSelection.getDeploymentSelection)(ctx, { url: void 0, adminKey: void 0, envFile: void 0 }); if (refParam !== void 0) { if (refParam === "local") { const cloudOnlyFlags = ["type", "region", "class", "default"]; for (const flag of cloudOnlyFlags) { if (options[flag]) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `--${flag} cannot be used when creating a local deployment` }); } } if (expiration !== void 0) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `--expiration cannot be used when creating a local deployment` }); } await createLocalDeployment( ctx, currentDeployment, options.select ?? false ); return; } } const expiresAt = await resolveExpiresAtOrCrash(ctx, expiration); const { ref, regionDetails, classDetails, projectId, type, isDefault, teamSlug, projectSlug } = process.stdin.isTTY ? await resolveOptionsInteractively( ctx, currentDeployment, refParam, options ) : await resolveOptionsNoninteractively( ctx, currentDeployment, refParam, options ); (0, import_log.showSpinner)( `Creating ${type} deployment` + (regionDetails ? ` in region ${regionDetails.displayName}` : "") + (classDetails ? ` with class ${classDetails.type}` : "") + "..." ); const created = (await (0, import_utils.typedPlatformClient)(ctx).POST( "/projects/{project_id}/create_deployment", { params: { path: { project_id: projectId } }, body: { type, region: regionDetails?.name ?? null, reference: ref ?? null, isDefault, ...expiresAt !== void 0 ? { expiresAt } : {}, ...classDetails ? { class: classDetails.type } : {} } } )).data; if (created.kind !== "cloud") { const err = `Expected cloud deployment to be created but got ${created.kind}`; return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: err, errForSentry: err }); } if (!options.select) { (0, import_log.logFinishedStep)( `Provisioned a ${created.isDefault ? "default " : ""}${created.deploymentType} deployment.` ); if (type !== "prod") { const selectRef = `${teamSlug}:${projectSlug}:${created.reference}`; (0, import_log.logMessage)( ` To make \`npx convex\` use this deployment, run ${import_chalk.chalkStderr.bold(`npx convex deployment select ${selectRef}`)}` ); (0, import_log.logMessage)( import_chalk.chalkStderr.gray( "Hint: use `--select` to immediately select the newly created deployment." ) ); } } if (options.select) { const selection = { kind: "deploymentWithinProject", targetProject: { kind: "teamAndProjectSlugs", teamSlug, projectSlug }, selectionWithinProject: { kind: "deploymentSelector", selector: created.reference } }; await (0, import_deploymentSelect.saveSelectedDeployment)( ctx, created.reference, selection, (0, import_deploymentSelection.deploymentNameFromSelection)(currentDeployment) ); } }); async function createLocalDeployment(ctx, currentDeployment, select) { const existing = (0, import_filePaths.loadProjectLocalConfig)(ctx); if (existing) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "A local deployment already exists." }); } const { teamSlug, slug: projectSlug } = await resolveProject( ctx, currentDeployment ); (0, import_log.showSpinner)("Downloading local backend..."); const { version } = await (0, import_download.ensureBackendBinaryDownloaded)(ctx, { kind: "latest" }); const { cloudPort, sitePort } = await (0, import_utils2.chooseLocalBackendPorts)(ctx); (0, import_log.showSpinner)("Registering local deployment..."); const { deploymentName, adminKey } = await (0, import_bigBrain.bigBrainStart)(ctx, { port: cloudPort, projectSlug, teamSlug, instanceName: null }); (0, import_filePaths.saveDeploymentConfig)(ctx, "local", deploymentName, { backendVersion: version, ports: { cloud: cloudPort, site: sitePort }, adminKey, instanceSecret: import_utils2.LOCAL_BACKEND_INSTANCE_SECRET }); (0, import_log.logFinishedStep)("Created local deployment."); await (0, import_localDeployment.importDefaultEnvVars)(ctx, { teamSlug, projectSlug, deploymentName, deploymentUrl: (0, import_run.localDeploymentUrl)(cloudPort), adminKey }); if (select) { const selection = { kind: "deploymentWithinProject", targetProject: { kind: "deploymentName", deploymentName, deploymentType: "local" }, selectionWithinProject: { kind: "deploymentSelector", selector: "local" } }; await (0, import_deploymentSelect.saveSelectedDeployment)( ctx, "local", selection, (0, import_deploymentSelection.deploymentNameFromSelection)(currentDeployment) ); } const devCommand = "npx convex dev"; if (select) { (0, import_log.logMessage)(` Run ${import_chalk.chalkStderr.bold(devCommand)} to start it.`); } else { (0, import_log.logMessage)( ` To use this deployment, run: ` + import_chalk.chalkStderr.bold(` npx convex deployment select local `) + ` Then, run ${import_chalk.chalkStderr.bold(devCommand)} to start it.` ); } } async function resolveOptionsNoninteractively(ctx, currentDeployment, refParam, options) { let ref; let teamAndProject; if (refParam) { const result = parseSelectorForNewDeployment(refParam); if (result.kind === "invalid") { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: result.message }); } ref = result.ref; teamAndProject = result.teamAndProject; } if (!ref && !options.default) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "Specify a deployment ref or use --default:\n `npx convex deployment create my-deployment-ref --type dev`\n `npx convex deployment create --type prod --default`" }); } if (!options.type) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `--type is required (supported values: ${SUPPORTED_TYPES.join(", ")})` }); } const project = teamAndProject ? await (0, import_deploymentSelection.getProjectDetails)(ctx, { kind: "teamAndProjectSlugs", teamSlug: teamAndProject.teamSlug, projectSlug: teamAndProject.projectSlug }) : await resolveProject(ctx, currentDeployment); const projectId = project.id; let regionDetails = null; if (options.region) { const availableRegions = await fetchAvailableRegions(ctx, project.teamId); regionDetails = await resolveRegionDetailsOrCrash( ctx, availableRegions, options.region ); } let classDetails = null; if (options.class) { const availableClasses = await fetchAvailableClasses(ctx, project.teamId); classDetails = await resolveClassDetailsOrCrash( ctx, availableClasses, options.class ); } return { ref, isDefault: options.default ?? null, projectId, regionDetails, classDetails, type: options.type, teamSlug: project.teamSlug, projectSlug: project.slug }; } async function resolveOptionsInteractively(ctx, currentDeployment, refParam, options) { let deploymentType; if (options.type) { deploymentType = logAndUse("type", options.type); } else { const dtypeChoices = [ { name: "dev", value: "dev" }, { name: "preview", value: "preview" }, { name: "prod", value: "prod" } ]; deploymentType = await (0, import_prompts.promptOptions)(ctx, { message: "Deployment type?", choices: dtypeChoices }); } let ref; let teamAndProject; if (refParam) { const result = parseSelectorForNewDeployment(refParam); if (result.kind === "invalid") { (0, import_log.logFailure)(result.message); } else { ref = logAndUse("ref", result.ref); teamAndProject = result.teamAndProject; } } while (ref === void 0) { const gitDefault = defaultRef(localGitBranch(), deploymentType); const input = await (0, import_prompts.promptString)(ctx, { message: "What do you want to call this deployment?\n" + import_chalk.chalkStderr.reset.dim( "The deployment reference will be used to identify your deployment on the dashboard and in CLI commands.\nExamples: staging, dev/james/feature" ) + "\n>", ...gitDefault !== void 0 ? { default: gitDefault } : {}, validate: validateTentativeReference }); const result = parseSelectorForNewDeployment(input); if (result.kind === "invalid") { (0, import_log.logFailure)(result.message); continue; } ref = result.ref; teamAndProject = result.teamAndProject; } const project = teamAndProject ? await (0, import_deploymentSelection.getProjectDetails)(ctx, { kind: "teamAndProjectSlugs", teamSlug: teamAndProject.teamSlug, projectSlug: teamAndProject.projectSlug }) : await resolveProject(ctx, currentDeployment); const availableRegions = await fetchAvailableRegions(ctx, project.teamId); let regionDetails; if (options.region) { regionDetails = await resolveRegionDetailsOrCrash( ctx, availableRegions, options.region ); logAndUse("region", regionDetails.displayName); } else { const teams = (await (0, import_utils.typedBigBrainClient)(ctx).GET("/teams")).data; const team = teams.find((team2) => team2.slug === project.teamSlug); if (!team) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Error: Team ${project.teamSlug} not found.` }); } const regionName = team.defaultRegion ?? await (0, import_utils.selectRegion)(ctx, team.id, deploymentType); regionDetails = await resolveRegionDetailsOrCrash( ctx, availableRegions, regionName ); if (team.defaultRegion) { (0, import_log.logFinishedStep)( `Using team default region of ${regionDetails.displayName}` ); } else { await (0, import_utils.logNoDefaultRegionMessage)(team.slug); } } let classDetails = null; if (options.class) { const availableClasses = await fetchAvailableClasses(ctx, project.teamId); classDetails = await resolveClassDetailsOrCrash( ctx, availableClasses, options.class ); logAndUse("class", classDetails.type); } return { ref, isDefault: options.default ?? null, projectId: project.id, regionDetails, classDetails, type: deploymentType, teamSlug: project.teamSlug, projectSlug: project.slug }; } function parseSelectorForNewDeployment(selectorString) { const selector = (0, import_deploymentSelector.parseDeploymentSelector)(selectorString); switch (selector.kind) { case "local": return { kind: "invalid", message: `"local" is reserved as an alias for your local deployment. To create one, run ${import_chalk.chalkStderr.bold("npx convex deployment create local")}` }; case "deploymentName": return { kind: "invalid", message: `"${selector.deploymentName}" is not a valid deployment reference. References can't look like "word-word-123" \u2014 that format is reserved for automatically-generated deployment names.` }; case "inCurrentProject": { const inner = selector.selector; if (inner.kind === "dev") { return { kind: "invalid", message: `"dev" is reserved as an alias for your default dev deployment.` }; } if (inner.kind === "prod") { return { kind: "invalid", message: `"prod" is reserved as an alias for your default production deployment.` }; } return { kind: "valid", ref: inner.reference }; } case "inProject": { return { kind: "invalid", message: `Please use "team:project:ref" to specify the team when creating a new deployment in a different project.` }; } case "inTeamProject": { const inner = selector.selector; if (inner.kind === "dev") { return { kind: "invalid", message: `"dev" is reserved as an alias for your default dev deployment.` }; } if (inner.kind === "prod") { return { kind: "invalid", message: `"prod" is reserved as an alias for your default production deployment.` }; } return { kind: "valid", ref: inner.reference, teamAndProject: { teamSlug: selector.teamSlug, projectSlug: selector.projectSlug } }; } default: selector; return { kind: "invalid", message: "Unknown state. This is a bug in Convex." }; } } async function resolveProject(ctx, deploymentSelection) { switch (deploymentSelection.kind) { case "existingDeployment": { const { deploymentFields } = deploymentSelection.deploymentToActOn; if (deploymentFields) { return await (0, import_deploymentSelection.getProjectDetails)(ctx, { kind: "deploymentName", deploymentName: deploymentFields.deploymentName, deploymentType: null }); } return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "Cannot infer project from the current deployment configuration. Use `team:project:ref` to specify team and project slugs." }); } case "deploymentWithinProject": { return await (0, import_deploymentSelection.getProjectDetails)(ctx, deploymentSelection.targetProject); } case "preview": { const slugs = await (0, import_deployment.getTeamAndProjectFromPreviewAdminKey)( ctx, deploymentSelection.previewDeployKey ); return await (0, import_deploymentSelection.getProjectDetails)(ctx, { kind: "teamAndProjectSlugs", teamSlug: slugs.teamSlug, projectSlug: slugs.projectSlug }); } case "chooseProject": return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "No project configured yet. Use `team:project:ref` to specify team and project slugs." }); case "anonymous": return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "Cannot create a deployment in anonymous mode. Run `npx convex login` and configure a project first." }); default: { deploymentSelection; return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Unexpected deployment selection kind.` }); } } } const REGION_NAME_TO_ALIAS = { "aws-us-east-1": "us", "aws-eu-west-1": "eu" }; const REGION_ALIAS_TO_NAME = Object.fromEntries( Object.entries(REGION_NAME_TO_ALIAS).map(([name, alias]) => [alias, name]) ); async function fetchAvailableRegions(ctx, teamId) { const regionsResponse = (await (0, import_utils.typedPlatformClient)(ctx).GET( "/teams/{team_id}/list_deployment_regions", { params: { path: { team_id: `${teamId}` } } } )).data; return regionsResponse.items.filter((item) => item.available); } function resolveRegionDetails(availableRegions, region) { const resolvedRegion = REGION_ALIAS_TO_NAME[region] ?? region; return availableRegions.find((item) => item.name === resolvedRegion) ?? null; } async function resolveRegionDetailsOrCrash(ctx, availableRegions, region) { const regionDetails = resolveRegionDetails(availableRegions, region); if (!regionDetails) { return await crashInvalidRegion(ctx, availableRegions, region); } return regionDetails; } function invalidRegionMessage(availableRegions, region) { const formatted = availableRegions.map( (item) => ` Use \`--region ${REGION_NAME_TO_ALIAS[item.name] ?? item.name}\` for ${item.displayName}` ).join("\n"); return `Invalid region "${region}". ` + formatted; } async function crashInvalidRegion(ctx, availableRegions, region) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: invalidRegionMessage(availableRegions, region) }); } async function fetchAvailableClasses(ctx, teamId) { const classesResponse = (await (0, import_utils.typedPlatformClient)(ctx).GET( "/teams/{team_id}/list_deployment_classes", { params: { path: { team_id: `${teamId}` } } } )).data; return classesResponse.items.filter((item) => item.available); } function resolveClassDetails(availableClasses, className) { return availableClasses.find((item) => item.type === className) ?? null; } async function resolveClassDetailsOrCrash(ctx, availableClasses, className) { const classDetails = resolveClassDetails(availableClasses, className); if (!classDetails) { return await crashInvalidClass(ctx, availableClasses, className); } return classDetails; } function invalidClassMessage(availableClasses, className) { const formatted = availableClasses.map((item) => ` \`--class ${item.type}\``).join("\n"); return `Invalid class "${className}". Available classes: ` + formatted; } async function crashInvalidClass(ctx, availableClasses, className) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: invalidClassMessage(availableClasses, className) }); } async function resolveExpiresAtOrCrash(ctx, expiration) { if (!expiration) { return void 0; } const parsed = (0, import_expiration.parseExpiration)(expiration); if (parsed.kind === "error") { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: parsed.message }); } const now = Date.now(); const resolved = (0, import_expiration.resolveExpiration)(parsed, now); if (resolved !== null) { const validation = (0, import_expiration.validateExpiration)(resolved, now); if (validation.kind === "error") { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: validation.message }); } } return resolved; } function logAndUse(label, value) { (0, import_log.logFinishedStep)(`Using ${label}: ${import_chalk.chalkStderr.bold(value)}`); return value; } function validateTentativeReference(tentativeReference) { if (tentativeReference.length < 3) { return "References must be at least 3 characters"; } if (tentativeReference.length > 100) { return "References must be at most 100 characters"; } if (!/^[a-z0-9/-]+$/.test(tentativeReference)) { return "References can only contain lowercase letters, numbers, `-`, and `/`"; } if (tentativeReference === "dev") { return '"dev" is reserved as an alias for your default dev deployment.'; } if (tentativeReference === "prod") { return '"prod" is reserved as an alias for your default production deployment.'; } if (tentativeReference === "local") { return `"local" is reserved as an alias for your local deployment. To create one, run ${import_chalk.chalkStderr.bold("npx convex deployment create local")}`; } if (/^[a-z]+-[a-z]+-\d+$/.test(tentativeReference)) { return `References can't look like "word-word-123" \u2014 that format is reserved for automatically-generated deployment names. Try something like dev/my-feature or staging instead.`; } return true; } function localGitBranch() { try { const branch = (0, import_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }).toString().trim(); if (!branch || branch === "HEAD" || branch === "main" || branch === "master") { return null; } return branch; } catch { return null; } } function defaultRef(branch, deploymentType) { if (deploymentType !== "dev" && deploymentType !== "preview") { return void 0; } if (!branch) return void 0; const slug = branch.replace(/[^a-z0-9/-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); if (!slug) return void 0; const ref = `${deploymentType}/${slug}`; const valid = validateTentativeReference(ref); if (valid !== true) return void 0; return ref; } //# sourceMappingURL=deploymentCreate.js.map