convex
Version:
Client for the Convex Cloud
274 lines (258 loc) • 8.6 kB
text/typescript
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<ReturnType<typeof oneoffContext>>;
functionName: string | undefined;
argsString: string | undefined;
options: {
inlineQuery?: string;
watch?: boolean;
push?: boolean;
identity?: string;
component?: string;
};
}): Promise<RunTarget> {
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 <functionName> 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 <functionName> 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 <functionName> --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<ReturnType<typeof oneoffContext>>;
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<ReturnType<typeof oneoffContext>>;
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.",
});
}