import { Command } from "@commander-js/extra-typings"; import { chalkStderr } from "chalk"; import { logMessage, logOutput } from "../bundler/log.js"; import { oneoffContext } from "../bundler/context.js"; import { jsonToConvex, type JSONValue } from "../values/value.js"; import { loadSelectedDeploymentCredentials } from "./lib/api.js"; import { actionDescription } from "./lib/command.js"; import { formatValue, pushToDeployment, runInDeployment, runSystemQuery, } from "./lib/run.js"; import { ensureHasConvexDependency } from "./lib/utils/utils.js"; import { getDeploymentSelection } from "./lib/deploymentSelection.js"; import { withRunningBackend } from "./lib/localDeployment/run.js"; import { inlineQueryToQuerySource, runTestFunctionQuery, } from "./lib/runTestFunction.js"; import { logAndHandleFetchError, ThrowingFetchError, } from "./lib/utils/utils.js"; export const run = new Command("run") .description( "Run a function or evaluate an inline readonly query on your deployment", ) .allowExcessArguments(false) .addRunOptions() .addDeploymentSelectionOptions(actionDescription("Run the function on")) .showHelpAfterError() .action(async (functionName, argsString, options) => { const ctx = await oneoffContext(options); const target = await resolveRunTarget({ ctx, functionName, argsString, options, }); if (target.kind === "function" || options.push) { await ensureHasConvexDependency(ctx, "run"); } const deploymentSelection = await getDeploymentSelection(ctx, options); const deployment = await loadSelectedDeploymentCredentials( ctx, deploymentSelection, { ensureLocalRunning: false }, ); if ( deployment.deploymentFields?.deploymentType === "prod" && options.push ) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `\`convex run\` doesn't support pushing functions to prod deployments. ` + `Remove the --push flag. To push to production use \`npx convex deploy\`.`, }); } await withRunningBackend({ ctx, deployment: { deploymentUrl: deployment.url, deploymentFields: deployment.deploymentFields, }, action: async () => { if (target.kind === "inlineQuery") { if (options.push) { await pushToDeployment(ctx, { deploymentUrl: deployment.url, adminKey: deployment.adminKey, deploymentName: deployment.deploymentFields?.deploymentName ?? null, typecheck: options.typecheck, typecheckComponents: options.typecheckComponents, codegen: options.codegen === "enable", liveComponentSources: Boolean(options.liveComponentSources), }); } return await runInlineQueryInDeployment({ ctx, deploymentUrl: deployment.url, adminKey: deployment.adminKey, inlineQuery: target.inlineQuery, ...(options.component !== undefined ? { componentPath: options.component } : {}), }); } await runInDeployment(ctx, { deploymentUrl: deployment.url, adminKey: deployment.adminKey, deploymentName: deployment.deploymentFields?.deploymentName ?? null, functionName: target.functionName, argsString: target.argsString, componentPath: options.component, identityString: options.identity, push: Boolean(options.push), watch: Boolean(options.watch), typecheck: options.typecheck, typecheckComponents: options.typecheckComponents, codegen: options.codegen === "enable", liveComponentSources: Boolean(options.liveComponentSources), }); }, }); }); type RunTarget = | { kind: "function"; functionName: string; argsString: string; } | { kind: "inlineQuery"; inlineQuery: string; }; async function resolveRunTarget(args: { ctx: Awaited>; functionName: string | undefined; argsString: string | undefined; options: { inlineQuery?: string; watch?: boolean; push?: boolean; identity?: string; component?: string; }; }): Promise { const inlineQuery = args.options.inlineQuery?.trim(); if (inlineQuery !== undefined && args.functionName !== undefined) { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "`npx convex run` accepts either or `--inline-query`, not both.", }); } if (inlineQuery === undefined) { if (args.functionName === undefined) { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "`npx convex run` requires either or `--inline-query`.", }); } return { kind: "function", functionName: args.functionName, argsString: args.argsString ?? "{}", }; } if (inlineQuery.length === 0) { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "`--inline-query` must not be empty.", }); } if (args.options.watch) { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "`--inline-query` can't be combined with `--watch`. Use `convex run --watch` for named deployed queries.", }); } if (args.options.identity !== undefined) { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "`--inline-query` can't be combined with `--identity`.", }); } return { kind: "inlineQuery", inlineQuery }; } async function runInlineQueryInDeployment(args: { ctx: Awaited>; deploymentUrl: string; adminKey: string; inlineQuery: string; componentPath?: string; }) { try { const componentId = await resolveInlineQueryComponentId(args); const outcome = await runTestFunctionQuery(args.ctx, { deploymentUrl: args.deploymentUrl, adminKey: args.adminKey, querySource: inlineQueryToQuerySource(args.inlineQuery), ...(componentId !== undefined ? { componentId } : {}), }); if (outcome.kind === "applicationFailure") { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: chalkStderr.red( `Query failed: ${JSON.stringify(outcome.payload, null, 2)}`, ), }); } for (const line of outcome.logLines) { logMessage(line); } const convexValue = jsonToConvex(outcome.value as JSONValue); if (convexValue !== null) logOutput(formatValue(convexValue)); } catch (err) { if (err instanceof ThrowingFetchError) return await err.handle(args.ctx); return await logAndHandleFetchError(args.ctx, err); } } async function resolveInlineQueryComponentId(args: { ctx: Awaited>; deploymentUrl: string; adminKey: string; componentPath?: string; }) { const componentPath = args.componentPath?.trim(); if (componentPath === undefined || componentPath === "_App") { return undefined; } if (componentPath.length === 0) { return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "`--component` must not be empty.", }); } const components = (await runSystemQuery(args.ctx, { deploymentUrl: args.deploymentUrl, adminKey: args.adminKey, functionName: "_system/frontend/components:list", componentPath: undefined, args: {}, })) as { id: string; path: string }[]; const component = components.find(({ path }) => path === componentPath); if (component !== undefined) { return component.id; } const availableComponents = components .map(({ path }) => path) .filter((path) => path.length > 0) .sort(); const availableComponentsMessage = availableComponents.length === 0 ? "This deployment has no mounted components." : `Available components:\n${availableComponents.map((path) => `• ${chalkStderr.gray(path)}`).join("\n")}`; return await args.ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Component path "${componentPath}" was not found.\n\n` + `${availableComponentsMessage}\n` + "Omit `--component` to target the app root.", }); }