UNPKG

convex

Version:

Client for the Convex Cloud

400 lines (399 loc) 11.7 kB
"use strict"; import axios from "axios"; import chalk from "chalk"; import inquirer from "inquirer"; import * as readline from "readline"; import path from "path"; import os from "os"; import { z } from "zod"; import { configFilepath } from "./config.js"; import { init } from "./init.js"; import { version } from "../../index.js"; export const productionProvisionHost = "https://provision.convex.dev"; export const provisionHost = process.env.CONVEX_PROVISION_HOST || productionProvisionHost; const BIG_BRAIN_URL = `${provisionHost}/api/${version}`; export function prompt(query) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise( (resolve) => rl.question(query, (answer) => { rl.close(); resolve(answer); }) ); } export async function fatalServerErr(ctx, err) { if (ctx.spinner) { ctx.spinner.fail(); } const res = err.response; if (res) { await deprecationCheckError(ctx, res); console.error( chalk.gray( `${res.status} ${res.statusText}: ${res.data.code}: ${res.data.message}` ) ); if (res.status == 401) { console.error( chalk.red("Log in to get an access token with `npx convex login`.") ); } } else { console.error(chalk.gray(err)); } return await ctx.fatalError(1, "network", err); } async function deprecationCheckError(ctx, resp) { if (ctx.deprecationMessagePrinted) { return; } const headers = resp.headers; if (headers) { const deprecationState = headers["x-convex-deprecation-state"]; const deprecationMessage = headers["x-convex-deprecation-message"]; switch (deprecationState) { case void 0: break; case "Upgradable": console.log(chalk.yellow(deprecationMessage)); break; case "Deprecated": case "UpgradeCritical": console.log(chalk.red(deprecationMessage)); return await ctx.fatalError(1, "network"); default: console.log(deprecationMessage); break; } } ctx.deprecationMessagePrinted = true; } export function deprecationCheckWarning(ctx, resp) { if (ctx.deprecationMessagePrinted) { return; } const headers = resp.headers; if (headers) { const deprecationState = headers["x-convex-deprecation-state"]; const deprecationMessage = headers["x-convex-deprecation-message"]; switch (deprecationState) { case void 0: break; case "Deprecated": case "UpgradeCritical": throw new Error( "Called deprecationCheckWarning on a fatal error. This is a bug." ); case "Upgradable": console.log(chalk.yellow(deprecationMessage)); break; default: console.log(deprecationMessage); break; } } ctx.deprecationMessagePrinted = true; } export async function validateOrSelectTeam(ctx, teamSlug, promptMessage) { const teams = await bigBrainAPI(ctx, "GET", "teams"); if (teams.length == 0) { console.error(chalk.red("Error: No teams found")); throw new Error("No teams found"); } if (!teamSlug) { switch (teams.length) { case 1: return teams[0].slug; default: return (await inquirer.prompt([ { name: "teamSlug", message: promptMessage, type: "list", choices: teams.map((team) => ({ name: `${team.name} (${team.slug})`, value: team.slug })) } ])).teamSlug; } } else { if (!teams.find((team) => team.slug == teamSlug)) { console.error(chalk.red(`Error: Team ${teamSlug} not found`)); throw new Error("Team not found"); } return teamSlug; } } export async function validateOrSelectProject(ctx, projectSlug, teamSlug, singleProjectPrompt, multiProjectPrompt) { const projects = await bigBrainAPI(ctx, "GET", `/teams/${teamSlug}/projects`); if (projects.length == 0) { console.error(chalk.red("Error: No projects found")); throw new Error("No projects found"); } if (!projectSlug) { switch (projects.length) { case 1: { console.log("Found 1 project."); const project = projects[0]; const confirmed = (await inquirer.prompt([ { type: "confirm", name: "confirmed", message: `${singleProjectPrompt} ${project.name} (${project.slug})?` } ])).confirmed; if (!confirmed) { return null; } return projects[0].slug; } default: console.log(`Found ${projects.length} projects.`); return (await inquirer.prompt([ { name: "project", message: multiProjectPrompt, type: "list", choices: projects.map((project) => ({ name: `${project.name} (${project.slug})`, value: project.slug })) } ])).project; } } else { if (!projects.find((project) => project.slug == projectSlug)) { console.error(chalk.red(`Error: Project ${projectSlug} not found`)); throw new Error("Project not found"); } return projectSlug; } } class PackageJsonLoadError extends Error { } export async function loadPackageJson(ctx) { let packageJson; try { packageJson = ctx.fs.readUtf8File("package.json"); } catch (err) { console.error( chalk.red( `Unable to read your package.json: ${err}. Make sure you're running this command from the root directory of a Convex app that contains the package.json` ) ); return await ctx.fatalError(1, "fs"); } let obj; try { obj = JSON.parse(packageJson); } catch (err) { console.error(chalk.red(`Unable to parse package.json: ${err}`)); return await ctx.fatalError(1, "fs", err); } if (typeof obj !== "object") { throw new PackageJsonLoadError( "Expected to parse an object from package.json" ); } const packages = []; if (obj.dependencies) { for (const dep in obj.dependencies) { packages.push({ name: dep, version: obj.dependencies[dep] }); } } if (obj.devDependencies) { for (const dep in obj.devDependencies) { packages.push({ name: dep, version: obj.devDependencies[dep] }); } } return packages; } export async function ensureHasConvexDependency(ctx, cmd) { const packages = await loadPackageJson(ctx); const hasConvexDependency = !!packages.filter(({ name }) => name === "convex").length; if (!hasConvexDependency) { console.error( chalk.red( `In order to ${cmd}, add \`convex\` to your package.json dependencies.` ) ); return await ctx.fatalError(1, "fs"); } } export const sorted = (arr, key) => { const newArr = [...arr]; const cmp = (a, b) => { if (key(a) < key(b)) return -1; if (key(a) > key(b)) return 1; return 0; }; return newArr.sort(cmp); }; export function functionsDir(configPath, projectConfig) { return path.join(path.dirname(configPath), projectConfig.functions); } export function rootDirectory() { let dirName; if (process.env.CONVEX_PROVISION_HOST) { dirName = ".convex-test"; } else { dirName = ".convex"; } return path.join(os.homedir(), dirName); } export function globalConfigPath() { return path.join(rootDirectory(), "config.json"); } async function readGlobalConfig(ctx) { const configPath = globalConfigPath(); let configFile; try { configFile = ctx.fs.readUtf8File(configPath); } catch (err) { return null; } try { const schema = z.object({ accessToken: z.string().min(1) }); const config = schema.parse(JSON.parse(configFile)); return config; } catch (err) { console.error( chalk.red( `Failed to parse global config in ${configPath} with error ${err}.` ) ); return null; } } export async function getAuthHeader(ctx) { if (process.env.CONVEX_OVERRIDE_ACCESS_TOKEN) { return `Bearer ${process.env.CONVEX_OVERRIDE_ACCESS_TOKEN}`; } const globalConfig = await readGlobalConfig(ctx); if (globalConfig) { return `Bearer ${globalConfig.accessToken}`; } return null; } export async function bigBrainClient(ctx) { const authHeader = await getAuthHeader(ctx); const headers = authHeader ? { Authorization: authHeader } : {}; return axios.create({ headers, baseURL: BIG_BRAIN_URL }); } export async function bigBrainAPI(ctx, method, url, data) { let res; try { const client = await bigBrainClient(ctx); res = await client.request({ url, method, data }); deprecationCheckWarning(ctx, res); return res.data; } catch (err) { return await fatalServerErr(ctx, err); } } export const poll = async function(fetch, condition, waitMs = 1e3) { let result = await fetch(); while (!condition(result)) { await wait(waitMs); result = await fetch(); } return result; }; const wait = function(waitMs) { return new Promise((resolve) => { setTimeout(resolve, waitMs); }); }; export function formatSize(n) { if (n < 1024) { return `${n} B`; } if (n < 1024 * 1024) { return `${Math.floor(n / 1024)} KB`; } if (n < 1024 * 1024 * 1024) { return `${Math.floor(n / 1024 / 1024)} MB`; } return `${n} B`; } export function formatDuration(ms) { const twoDigits = (n, unit) => `${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}${unit}`; if (ms < 1e-3) { return twoDigits(ms * 1e9, "ns"); } if (ms < 1) { return twoDigits(ms * 1e3, "\xB5s"); } if (ms < 1e3) { return twoDigits(ms, "ms"); } const s = ms / 1e3; if (s < 60) { return twoDigits(ms / 1e3, "s"); } return twoDigits(s / 60, "m"); } function findParentConfigs(ctx) { const parentPackageJson = findUp(ctx, "package.json"); const candidateConvexJson = parentPackageJson && path.join(path.dirname(parentPackageJson), "convex.json"); const parentConvexJson = candidateConvexJson && ctx.fs.exists(candidateConvexJson) ? candidateConvexJson : void 0; return { parentPackageJson, parentConvexJson }; } function findUp(ctx, filename) { let curDir = path.resolve("."); let parentDir = curDir; do { const candidate = path.join(curDir, filename); if (ctx.fs.exists(candidate)) { return candidate; } curDir = parentDir; parentDir = path.dirname(curDir); } while (parentDir !== curDir); return; } export async function ensureProjectDirectory(ctx, ensureConvexJson = false) { const { parentPackageJson, parentConvexJson } = findParentConfigs(ctx); if (!parentPackageJson) { console.error( "No package.json found. If you meant to create a new project, try" ); console.error(`npx create-next-app@latest -e convex my-convex-app`); await ctx.fatalError(1); } if (parentPackageJson !== path.resolve("package.json")) { console.error("Run this command from the root directory of a project."); return await ctx.fatalError(1, "fs"); } if (ensureConvexJson && parentPackageJson && !parentConvexJson) { const expected = await configFilepath(ctx); console.error(`No convex.json file found at ${expected}`); const { confirmed } = await inquirer.prompt([ { type: "confirm", name: "confirmed", message: `Would you like to create a new Convex project here? (\`npx convex init\`)` } ]); if (!confirmed) { console.error("Run `npx convex dev` in a directory with a convex.json."); return await ctx.fatalError(1, "fs"); } await init(ctx, null, null); } } //# sourceMappingURL=utils.js.map