convex
Version:
Client for the Convex Cloud
132 lines (115 loc) • 4.15 kB
text/typescript
import type { Context } from "../../bundler/context.js";
import { deploymentFetch } from "./utils/utils.js";
const QUERY_MODULE_PREAMBLE =
'import { query, internalQuery } from "convex:/_system/repl/wrappers.js";';
/** Shared help text for the query/module string (CLI argument + MCP input). */
export const RUN_ONEOFF_QUERY_SOURCE_DESCRIPTION =
'JavaScript module source for a single file (testQuery.js) that exports a default readonly query, for example: export default query({ handler: async (ctx) => ({ count: (await ctx.db.query("messages").take(10)).length }) });';
export const INLINE_QUERY_DESCRIPTION =
"JavaScript to evaluate as a readonly query, for example: 'await ctx.db.query(\"messages\").take(5)'. Simple expressions are returned automatically. For multi-statement queries, use an explicit return. Full `export default query(...)` modules are also supported. This is a one-shot query and cannot be combined with `--watch`. Use `--component` to target a mounted component. For more examples, see `npx convex docs`.";
export type RunTestFunctionQuerySuccess = {
kind: "success";
value: unknown;
logLines: string[];
};
export type RunTestFunctionQueryApplicationFailure = {
kind: "applicationFailure";
payload: unknown;
};
export type RunTestFunctionQueryResult =
| RunTestFunctionQuerySuccess
| RunTestFunctionQueryApplicationFailure;
export function inlineQueryToQuerySource(inlineQuery: string) {
const trimmedQuery = inlineQuery.trim();
if (looksLikeQueryModuleSource(trimmedQuery)) {
return injectQueryModulePreamble(trimmedQuery);
}
const queryBody = inlineQueryBody(trimmedQuery);
return `${QUERY_MODULE_PREAMBLE}
export default query({
handler: async (ctx) => {
${indent(queryBody, 4)}
},
});`;
}
/**
* POST /api/run_test_function with the same body shape as the dashboard and MCP.
* Uses deploymentFetch for Convex-Client, auth headers, retries, and error typing.
* On HTTP failure, throws ThrowingFetchError (from deploymentFetch).
*/
export async function runTestFunctionQuery(
ctx: Context,
args: {
deploymentUrl: string;
adminKey: string;
querySource: string;
componentId?: string;
},
): Promise<RunTestFunctionQueryResult> {
const fetchDeployment = deploymentFetch(ctx, {
deploymentUrl: args.deploymentUrl,
adminKey: args.adminKey,
});
const response = await fetchDeployment("/api/run_test_function", {
method: "POST",
body: JSON.stringify({
adminKey: args.adminKey,
args: {},
bundle: {
path: "testQuery.js",
source: args.querySource,
},
format: "convex_encoded_json",
...(args.componentId !== undefined
? { componentId: args.componentId }
: {}),
}),
});
const result: unknown = await response.json();
if (
typeof result !== "object" ||
result === null ||
!("status" in result) ||
(result as { status: string }).status !== "success"
) {
return { kind: "applicationFailure", payload: result };
}
const ok = result as {
status: "success";
value: unknown;
logLines?: string[];
};
return {
kind: "success",
value: ok.value,
logLines: ok.logLines ?? [],
};
}
function looksLikeQueryModuleSource(querySource: string) {
if (!querySource.includes("export default")) return false;
return /\b(?:query|internalQuery)\s*\(/.test(querySource);
}
function injectQueryModulePreamble(querySource: string) {
if (querySource.includes("convex:/_system/repl/wrappers.js"))
return querySource;
return `${QUERY_MODULE_PREAMBLE}
${querySource}`;
}
function inlineQueryBody(inlineQuery: string) {
const trimmed = inlineQuery.trim();
if (!isExpression(trimmed)) return trimmed;
return `return (${trimmed.replace(/;$/, "")});`;
}
function isExpression(inlineQuery: string) {
if (inlineQuery.includes("\n")) return false;
return !/^(const|let|var|if|for|while|switch|try|throw|return)\b/.test(
inlineQuery,
);
}
function indent(text: string, spaces: number) {
const prefix = " ".repeat(spaces);
return text
.split("\n")
.map((line) => `${prefix}${line}`)
.join("\n");
}