From 676fdddc25025904bbee583cf7e88e0195b28ad2 Mon Sep 17 00:00:00 2001 From: Sarah Shader Date: Fri, 3 Jan 2025 12:40:50 -0500 Subject: [PATCH] Improve usability of `npx convex run` (#32706) A few small quality of life improvements: * `npx convex run api.foo.bar` will be equivalent to `npx convex run foo:bar` * `npx convex run convex/foo/bar` will be equivalent to `npx convex run foo/bar:default` -- this is useful since the file path usually autocompletes in the terminal. * Support for `--identity` similar to the dashboard "acting as user" feature, so `npx convex run --identity '{ name: "sshader" }'` fakes out `auth.getUserIdentity` in my function as `{ name: "sshader" }` * And nicer JSON parsing using JSON5, so things like `{ name: "sshader" }` actually parses instead of requiring the keys to all be quoted GitOrigin-RevId: cab1d0469ab9133a019feecb885b12e861b07966 --- crates/local_backend/src/admin.rs | 18 +- .../common/config/rush/pnpm-lock.yaml | 3 + npm-packages/convex/package.json | 1 + npm-packages/convex/src/cli/convexExport.ts | 15 +- npm-packages/convex/src/cli/convexImport.ts | 15 +- npm-packages/convex/src/cli/data.ts | 24 +- npm-packages/convex/src/cli/deploy.ts | 17 +- npm-packages/convex/src/cli/dev.ts | 72 ++-- npm-packages/convex/src/cli/env.ts | 28 +- npm-packages/convex/src/cli/functionSpec.ts | 13 +- .../src/cli/lib/localDeployment/upgrade.ts | 15 +- npm-packages/convex/src/cli/lib/run.test.ts | 44 +++ npm-packages/convex/src/cli/lib/run.ts | 320 +++++++++++++----- npm-packages/convex/src/cli/run.ts | 28 +- 14 files changed, 403 insertions(+), 210 deletions(-) create mode 100644 npm-packages/convex/src/cli/lib/run.test.ts diff --git a/crates/local_backend/src/admin.rs b/crates/local_backend/src/admin.rs index b95a8a81..a09acebd 100644 --- a/crates/local_backend/src/admin.rs +++ b/crates/local_backend/src/admin.rs @@ -53,14 +53,18 @@ fn must_be_admin_internal( identity: &Identity, needs_write_access: bool, ) -> anyhow::Result { - if let Identity::InstanceAdmin(admin_identity) = identity { - if needs_write_access && admin_identity.is_read_only() { - return Err(read_only_admin_key_error().into()); - } - Ok(admin_identity.principal().clone()) - } else { - Err(bad_admin_key_error(identity.instance_name()).into()) + let admin_identity = match identity { + Identity::InstanceAdmin(admin_identity) => admin_identity, + Identity::ActingUser(admin_identity, _user_identity_attributes) => admin_identity, + Identity::System(_) | Identity::User(_) | Identity::Unknown => { + return Err(bad_admin_key_error(identity.instance_name()).into()); + }, + }; + + if needs_write_access && admin_identity.is_read_only() { + return Err(read_only_admin_key_error().into()); } + Ok(admin_identity.principal().clone()) } pub fn must_be_admin_member_with_write_access(identity: &Identity) -> anyhow::Result { diff --git a/npm-packages/common/config/rush/pnpm-lock.yaml b/npm-packages/common/config/rush/pnpm-lock.yaml index 166544c3..1f6eeeeb 100644 --- a/npm-packages/common/config/rush/pnpm-lock.yaml +++ b/npm-packages/common/config/rush/pnpm-lock.yaml @@ -238,6 +238,9 @@ importers: jsdom: specifier: ~25.0.1 version: 25.0.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + json5: + specifier: ~2.2.3 + version: 2.2.3 jwt-encode: specifier: ~1.0.1 version: 1.0.1 diff --git a/npm-packages/convex/package.json b/npm-packages/convex/package.json index 9d523009..56f33b53 100644 --- a/npm-packages/convex/package.json +++ b/npm-packages/convex/package.json @@ -309,6 +309,7 @@ "inquirer": "^9.1.4", "inquirer-search-list": "~1.2.6", "jsdom": "~25.0.1", + "json5": "~2.2.3", "jwt-encode": "~1.0.1", "knip": "~5.39.4", "napi-wasm": "1.1.3", diff --git a/npm-packages/convex/src/cli/convexExport.ts b/npm-packages/convex/src/cli/convexExport.ts index e9ee7aad..036059b0 100644 --- a/npm-packages/convex/src/cli/convexExport.ts +++ b/npm-packages/convex/src/cli/convexExport.ts @@ -140,15 +140,14 @@ async function waitForStableExportState( ): Promise { const [donePromise, onDone] = waitUntilCalled(); let snapshotExportState: SnapshotExportState; - await subscribe( - ctx, + await subscribe(ctx, { deploymentUrl, adminKey, - "_system/cli/exports:getLatest", - {}, - undefined, - donePromise, - { + parsedFunctionName: "_system/cli/exports:getLatest", + parsedFunctionArgs: {}, + componentPath: undefined, + until: donePromise, + callbacks: { onChange: (value: any) => { // NOTE: `value` would only be `null` if there has never been an export // requested. @@ -168,7 +167,7 @@ async function waitForStableExportState( } }, }, - ); + }); return snapshotExportState!; } diff --git a/npm-packages/convex/src/cli/convexImport.ts b/npm-packages/convex/src/cli/convexImport.ts index 7c7cba3b..55b8c2df 100644 --- a/npm-packages/convex/src/cli/convexImport.ts +++ b/npm-packages/convex/src/cli/convexImport.ts @@ -378,15 +378,14 @@ export async function waitForStableImportState( const [donePromise, onDone] = waitUntilCalled(); let snapshotImportState: SnapshotImportState; let checkpointCount = 0; - await subscribe( - ctx, + await subscribe(ctx, { deploymentUrl, adminKey, - "_system/cli/queryImport", - { importId }, - undefined, - donePromise, - { + parsedFunctionName: "_system/cli/queryImport", + parsedFunctionArgs: { importId }, + componentPath: undefined, + until: donePromise, + callbacks: { onChange: (value: any) => { snapshotImportState = value.state; switch (snapshotImportState.state) { @@ -409,7 +408,7 @@ export async function waitForStableImportState( } }, }, - ); + }); return snapshotImportState!; } diff --git a/npm-packages/convex/src/cli/data.ts b/npm-packages/convex/src/cli/data.ts index a0d01a5f..3d5cff89 100644 --- a/npm-packages/convex/src/cli/data.ts +++ b/npm-packages/convex/src/cli/data.ts @@ -13,7 +13,7 @@ import { deploymentSelectionFromOptions, fetchDeploymentCredentialsProvisionProd, } from "./lib/api.js"; -import { runPaginatedQuery } from "./lib/run.js"; +import { runSystemPaginatedQuery } from "./lib/run.js"; import { parsePositiveInteger } from "./lib/utils/utils.js"; import { Command } from "@commander-js/extra-typings"; import { actionDescription } from "./lib/command.js"; @@ -87,14 +87,13 @@ async function listTables( deploymentName: string | undefined, componentPath: string, ) { - const tables = (await runPaginatedQuery( - ctx, + const tables = (await runSystemPaginatedQuery(ctx, { deploymentUrl, adminKey, - "_system/cli/tables", + functionName: "_system/cli/tables", componentPath, - {}, - )) as { name: string }[]; + args: {}, + })) as { name: string }[]; if (tables.length === 0) { logError( ctx, @@ -120,18 +119,17 @@ async function listDocuments( componentPath: string; }, ) { - const data = (await runPaginatedQuery( - ctx, + const data = (await runSystemPaginatedQuery(ctx, { deploymentUrl, adminKey, - "_system/cli/tableData", - options.componentPath, - { + functionName: "_system/cli/tableData", + componentPath: options.componentPath, + args: { table: tableName, order: options.order ?? "desc", }, - options.limit + 1, - )) as Record[]; + limit: options.limit + 1, + })) as Record[]; if (data.length === 0) { logError(ctx, "There are no documents in this table."); diff --git a/npm-packages/convex/src/cli/deploy.ts b/npm-packages/convex/src/cli/deploy.ts index 38a1975b..7ca10fcf 100644 --- a/npm-packages/convex/src/cli/deploy.ts +++ b/npm-packages/convex/src/cli/deploy.ts @@ -252,14 +252,13 @@ async function deployToNewPreviewDeployment( logFinishedStep(ctx, `Deployed Convex functions to ${previewUrl}`); if (options.previewRun !== undefined) { - await runFunctionAndLog( - ctx, - previewUrl, - previewAdminKey, - options.previewRun, - {}, - undefined, - { + await runFunctionAndLog(ctx, { + deploymentUrl: previewUrl, + adminKey: previewAdminKey, + functionName: options.previewRun, + argsString: "{}", + componentPath: undefined, + callbacks: { onSuccess: () => { logFinishedStep( ctx, @@ -267,7 +266,7 @@ async function deployToNewPreviewDeployment( ); }, }, - ); + }); } } diff --git a/npm-packages/convex/src/cli/dev.ts b/npm-packages/convex/src/cli/dev.ts index e00cf5ff..80d1afed 100644 --- a/npm-packages/convex/src/cli/dev.ts +++ b/npm-packages/convex/src/cli/dev.ts @@ -375,19 +375,18 @@ async function runFunctionInDev( functionName: string, componentPath: string | undefined, ) { - await runFunctionAndLog( - ctx, - credentials.url, - credentials.adminKey, + await runFunctionAndLog(ctx, { + deploymentUrl: credentials.url, + adminKey: credentials.adminKey, functionName, - {}, + argsString: "{}", componentPath, - { + callbacks: { onSuccess: () => { logFinishedStep(ctx, `Finished running function "${functionName}"`); }, }, - ); + }); } function getTableWatch( @@ -399,13 +398,13 @@ function getTableWatch( tableName: string | null, componentPath: string | undefined, ) { - return getFunctionWatch( - ctx, - credentials, - "_system/cli/queryTable", - () => (tableName !== null ? { tableName } : null), + return getFunctionWatch(ctx, { + deploymentUrl: credentials.url, + adminKey: credentials.adminKey, + parsedFunctionName: "_system/cli/queryTable", + getArgs: () => (tableName !== null ? { tableName } : null), componentPath, - ); + }); } function getDeplymentEnvVarWatch( @@ -416,42 +415,41 @@ function getDeplymentEnvVarWatch( }, shouldRetryOnDeploymentEnvVarChange: boolean, ) { - return getFunctionWatch( - ctx, - credentials, - "_system/cli/queryEnvironmentVariables", - () => (shouldRetryOnDeploymentEnvVarChange ? {} : null), - undefined, - ); + return getFunctionWatch(ctx, { + deploymentUrl: credentials.url, + adminKey: credentials.adminKey, + parsedFunctionName: "_system/cli/queryEnvironmentVariables", + getArgs: () => (shouldRetryOnDeploymentEnvVarChange ? {} : null), + componentPath: undefined, + }); } function getFunctionWatch( ctx: WatchContext, - credentials: { - url: string; + args: { + deploymentUrl: string; adminKey: string; + parsedFunctionName: string; + getArgs: () => Record | null; + componentPath: string | undefined; }, - functionName: string, - getArgs: () => Record | null, - componentPath: string | undefined, ) { const [stopPromise, stop] = waitUntilCalled(); return { watch: async () => { - const args = getArgs(); - if (args === null) { + const functionArgs = args.getArgs(); + if (functionArgs === null) { return waitForever(); } let changes = 0; - return subscribe( - ctx, - credentials.url, - credentials.adminKey, - functionName, - args, - componentPath, - stopPromise, - { + return subscribe(ctx, { + deploymentUrl: args.deploymentUrl, + adminKey: args.adminKey, + parsedFunctionName: args.parsedFunctionName, + parsedFunctionArgs: functionArgs, + componentPath: args.componentPath, + until: stopPromise, + callbacks: { onChange: () => { changes++; // First bump is just the initial results reporting @@ -460,7 +458,7 @@ function getFunctionWatch( } }, }, - ); + }); }, stop: () => { stop(); diff --git a/npm-packages/convex/src/cli/env.ts b/npm-packages/convex/src/cli/env.ts index 31e1a19f..3daa99f8 100644 --- a/npm-packages/convex/src/cli/env.ts +++ b/npm-packages/convex/src/cli/env.ts @@ -14,7 +14,7 @@ import { fetchDeploymentCredentialsWithinCurrentProject, } from "./lib/api.js"; import { actionDescription } from "./lib/command.js"; -import { runQuery } from "./lib/run.js"; +import { runSystemQuery } from "./lib/run.js"; import { deploymentFetch, ensureHasConvexDependency, @@ -88,14 +88,13 @@ const envGet = new Command("get") deploymentSelection, ); - const envVar = (await runQuery( - ctx, - url, + const envVar = (await runSystemQuery(ctx, { + deploymentUrl: url, adminKey, - "_system/cli/queryEnvironmentVariables:get", - undefined, - { name: envVarName }, - )) as EnvVar | null; + functionName: "_system/cli/queryEnvironmentVariables:get", + componentPath: undefined, + args: { name: envVarName }, + })) as EnvVar | null; if (envVar === null) { logFailure(ctx, `Environment variable "${envVarName}" not found.`); return; @@ -141,14 +140,13 @@ const envList = new Command("list") deploymentSelection, ); - const envs = (await runQuery( - ctx, - url, + const envs = (await runSystemQuery(ctx, { + deploymentUrl: url, adminKey, - "_system/cli/queryEnvironmentVariables", - undefined, - {}, - )) as EnvVar[]; + functionName: "_system/cli/queryEnvironmentVariables", + componentPath: undefined, + args: {}, + })) as EnvVar[]; if (envs.length === 0) { logMessage(ctx, "No environment variables set."); return; diff --git a/npm-packages/convex/src/cli/functionSpec.ts b/npm-packages/convex/src/cli/functionSpec.ts index 6fcee31f..769c9abd 100644 --- a/npm-packages/convex/src/cli/functionSpec.ts +++ b/npm-packages/convex/src/cli/functionSpec.ts @@ -4,7 +4,7 @@ import { deploymentSelectionFromOptions, fetchDeploymentCredentialsWithinCurrentProject, } from "./lib/api.js"; -import { runQuery } from "./lib/run.js"; +import { runSystemQuery } from "./lib/run.js"; import { Command, Option } from "@commander-js/extra-typings"; import { actionDescription } from "./lib/command.js"; @@ -30,14 +30,13 @@ export const functionSpec = new Command("function-spec") deploymentSelection, ); - const functions = (await runQuery( - ctx, + const functions = (await runSystemQuery(ctx, { deploymentUrl, adminKey, - "_system/cli/modules:apiSpec", - undefined, - {}, - )) as any[]; + functionName: "_system/cli/modules:apiSpec", + componentPath: undefined, + args: {}, + })) as any[]; const output = JSON.stringify( { url: deploymentUrl, functions: functions }, diff --git a/npm-packages/convex/src/cli/lib/localDeployment/upgrade.ts b/npm-packages/convex/src/cli/lib/localDeployment/upgrade.ts index d5e0f03f..bd6c44fe 100644 --- a/npm-packages/convex/src/cli/lib/localDeployment/upgrade.ts +++ b/npm-packages/convex/src/cli/lib/localDeployment/upgrade.ts @@ -5,7 +5,7 @@ import { logFinishedStep, logVerbose, } from "../../../bundler/context.js"; -import { runQuery } from "../run.js"; +import { runSystemQuery } from "../run.js"; import { deploymentStateDir, saveDeploymentConfig } from "./filePaths.js"; import { ensureBackendBinaryDownloaded, @@ -141,14 +141,13 @@ async function handleUpgrade( logVerbose(ctx, "Downloading env vars"); const deploymentUrl = localDeploymentUrl(args.ports.cloud); - const envs = (await runQuery( - ctx, + const envs = (await runSystemQuery(ctx, { deploymentUrl, - args.adminKey, - "_system/cli/queryEnvironmentVariables", - undefined, - {}, - )) as Array<{ + adminKey: args.adminKey, + functionName: "_system/cli/queryEnvironmentVariables", + componentPath: undefined, + args: {}, + })) as Array<{ name: string; value: string; }>; diff --git a/npm-packages/convex/src/cli/lib/run.test.ts b/npm-packages/convex/src/cli/lib/run.test.ts new file mode 100644 index 00000000..cfdf41ee --- /dev/null +++ b/npm-packages/convex/src/cli/lib/run.test.ts @@ -0,0 +1,44 @@ +import { test, expect } from "vitest"; +import { parseFunctionName } from "./run.js"; +import { oneoffContext } from "../../bundler/context.js"; + +test("parseFunctionName", async () => { + const originalContext = oneoffContext(); + const files = new Set(); + const ctx = { + ...originalContext, + fs: { + ...originalContext.fs, + exists: (file: string) => files.has(file), + }, + }; + + files.add("convex/foo/bar.ts"); + files.add("convex/convex/bar/baz.ts"); + files.add("src/convex/foo/bar.ts"); + + expect(await parseFunctionName(ctx, "api.foo.bar", "convex/")).toEqual( + "foo:bar", + ); + expect(await parseFunctionName(ctx, "internal.foo.bar", "convex/")).toEqual( + "foo:bar", + ); + expect(await parseFunctionName(ctx, "foo/bar", "convex/")).toEqual( + "foo/bar:default", + ); + expect(await parseFunctionName(ctx, "foo/bar:baz", "convex/")).toEqual( + "foo/bar:baz", + ); + expect(await parseFunctionName(ctx, "convex/foo/bar", "convex/")).toEqual( + "foo/bar:default", + ); + expect(await parseFunctionName(ctx, "convex/bar/baz", "convex/")).toEqual( + "convex/bar/baz:default", + ); + expect( + await parseFunctionName(ctx, "src/convex/foo/bar", "src/convex/"), + ).toEqual("foo/bar:default"); + expect(await parseFunctionName(ctx, "foo/bar", "src/convex/")).toEqual( + "foo/bar:default", + ); +}); diff --git a/npm-packages/convex/src/cli/lib/run.ts b/npm-packages/convex/src/cli/lib/run.ts index 3fb4da37..7a0c3b3b 100644 --- a/npm-packages/convex/src/cli/lib/run.ts +++ b/npm-packages/convex/src/cli/lib/run.ts @@ -3,8 +3,12 @@ import util from "util"; import ws from "ws"; import { ConvexHttpClient } from "../../browser/http_client.js"; import { BaseConvexClient } from "../../browser/index.js"; -import { PaginationResult, makeFunctionReference } from "../../server/index.js"; -import { Value, convexToJson } from "../../values/value.js"; +import { + PaginationResult, + UserIdentityAttributes, + makeFunctionReference, +} from "../../server/index.js"; +import { Value, convexToJson, jsonToConvex } from "../../values/value.js"; import { Context, logFinishedStep, @@ -12,37 +16,53 @@ import { logOutput, } from "../../bundler/context.js"; import { waitForever, waitUntilCalled } from "./utils/utils.js"; +import JSON5 from "json5"; +import path from "path"; +import { readProjectConfig } from "./config.js"; export async function runFunctionAndLog( ctx: Context, - deploymentUrl: string, - adminKey: string, - functionName: string, - args: Value, - componentPath?: string, - callbacks?: { - onSuccess?: () => void; + args: { + deploymentUrl: string; + adminKey: string; + functionName: string; + argsString: string; + identityString?: string; + componentPath?: string; + callbacks?: { + onSuccess?: () => void; + }; }, ) { - const client = new ConvexHttpClient(deploymentUrl); - client.setAdminAuth(adminKey); + const client = new ConvexHttpClient(args.deploymentUrl); + const identity = args.identityString + ? await getFakeIdentity(ctx, args.identityString) + : undefined; + client.setAdminAuth(args.adminKey, identity); + const functionArgs = await parseArgs(ctx, args.argsString); + const { projectConfig } = await readProjectConfig(ctx); + const parsedFunctionName = await parseFunctionName( + ctx, + args.functionName, + projectConfig.functions, + ); let result: Value; try { result = await client.function( - makeFunctionReference(functionName), - componentPath, - args, + makeFunctionReference(parsedFunctionName), + args.componentPath, + functionArgs, ); } catch (err) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem or env vars", - printedMessage: `Failed to run function "${functionName}":\n${chalk.red((err as Error).toString().trim())}`, + printedMessage: `Failed to run function "${args.functionName}":\n${chalk.red((err as Error).toString().trim())}`, }); } - callbacks?.onSuccess?.(); + args.callbacks?.onSuccess?.(); // `null` is the default return type if (result !== null) { @@ -50,35 +70,143 @@ export async function runFunctionAndLog( } } -export async function runPaginatedQuery( +async function getFakeIdentity(ctx: Context, identityString: string) { + let identity: UserIdentityAttributes; + try { + identity = JSON5.parse(identityString); + } catch (err) { + return await ctx.crash({ + exitCode: 1, + errorType: "fatal", + printedMessage: `Failed to parse identity as JSON: "${identityString}"\n${chalk.red((err as Error).toString().trim())}`, + }); + } + const subject = identity.subject ?? "" + simpleHash(JSON.stringify(identity)); + const issuer = identity.issuer ?? "https://convex.test"; + const tokenIdentifier = + identity.tokenIdentifier ?? `${issuer.toString()}|${subject.toString()}`; + return { + ...identity, + subject, + issuer, + tokenIdentifier, + }; +} + +async function parseArgs(ctx: Context, argsString: string) { + try { + const argsJson = JSON5.parse(argsString); + return jsonToConvex(argsJson) as Record; + } catch (err) { + return await ctx.crash({ + exitCode: 1, + errorType: "invalid filesystem or env vars", + printedMessage: `Failed to parse arguments as JSON: "${argsString}"\n${chalk.red((err as Error).toString().trim())}`, + }); + } +} + +export async function parseFunctionName( ctx: Context, - deploymentUrl: string, - adminKey: string, functionName: string, - componentPath: string | undefined, - args: Record, - limit?: number, + // Usually `convex/` -- should contain trailing slash + functionDirName: string, +) { + // api.foo.bar -> foo:bar + // foo/bar -> foo/bar:default + // foo/bar:baz -> foo/bar:baz + // convex/foo/bar -> foo/bar:default + + // This is the `api.foo.bar` format + if (functionName.startsWith("api.") || functionName.startsWith("internal.")) { + const parts = functionName.split("."); + if (parts.length < 3) { + return await ctx.crash({ + exitCode: 1, + errorType: "fatal", + printedMessage: `Function name has too few parts: "${functionName}"`, + }); + } + const exportName = parts.pop(); + const parsedName = `${parts.slice(1).join("/")}:${exportName}`; + return parsedName; + } + + // This is the `foo/bar:baz` format + + // This is something like `convex/foo/bar`, which could either be addressing `foo/bar:default` or `convex/foo/bar:default` + // if there's a directory with the same name as the functions directory nested directly underneath. + // We'll prefer the `convex/foo/bar:default` version, and check if the file exists, and otherwise treat this as a relative path from the project root. + const filePath = functionName.split(":")[0]; + const exportName = functionName.split(":")[1] ?? "default"; + const normalizedName = `${filePath}:${exportName}`; + + // This isn't a relative path from the project root + if (!filePath.startsWith(functionDirName)) { + return normalizedName; + } + + const filePathWithoutPrefix = filePath.slice(functionDirName.length); + const functionNameWithoutPrefix = `${filePathWithoutPrefix}:${exportName}`; + + const possibleExtensions = [".ts", ".js", ".tsx", ".jsx"]; + const hasExtension = possibleExtensions.some((extension) => + filePath.endsWith(extension), + ); + if (hasExtension) { + if (ctx.fs.exists(path.join(functionDirName, filePath))) { + return normalizedName; + } else { + return functionNameWithoutPrefix; + } + } else { + const exists = possibleExtensions.some((extension) => + ctx.fs.exists(path.join(functionDirName, filePath + extension)), + ); + if (exists) { + return normalizedName; + } else { + return functionNameWithoutPrefix; + } + } +} + +function simpleHash(string: string) { + let hash = 0; + for (let i = 0; i < string.length; i++) { + const char = string.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +} + +export async function runSystemPaginatedQuery( + ctx: Context, + args: { + deploymentUrl: string; + adminKey: string; + functionName: string; + componentPath: string | undefined; + args: Record; + limit?: number; + }, ) { const results = []; let cursor = null; let isDone = false; - while (!isDone && (limit === undefined || results.length < limit)) { - const paginationResult = (await runQuery( - ctx, - deploymentUrl, - adminKey, - functionName, - componentPath, - { - ...args, - // The pagination is limited on the backend, so the 10000 - // means "give me as many as possible". + while (!isDone && (args.limit === undefined || results.length < args.limit)) { + const paginationResult = (await runSystemQuery(ctx, { + ...args, + args: { + ...args.args, paginationOpts: { cursor, - numItems: limit === undefined ? 10000 : limit - results.length, + numItems: + args.limit === undefined ? 10000 : args.limit - results.length, }, }, - )) as unknown as PaginationResult>; + })) as unknown as PaginationResult>; isDone = paginationResult.isDone; cursor = paginationResult.continueCursor; results.push(...paginationResult.page); @@ -86,34 +214,33 @@ export async function runPaginatedQuery( return results; } -export async function runQuery( +export async function runSystemQuery( ctx: Context, - deploymentUrl: string, - adminKey: string, - functionName: string, - componentPath: string | undefined, - args: Record, + args: { + deploymentUrl: string; + adminKey: string; + functionName: string; + componentPath: string | undefined; + args: Record; + }, ): Promise { let onResult: (result: Value) => void; const resultPromise = new Promise((resolve) => { onResult = resolve; }); const [donePromise, onDone] = waitUntilCalled(); - await subscribe( - ctx, - deploymentUrl, - adminKey, - functionName, - args, - componentPath, - donePromise, - { + await subscribe(ctx, { + ...args, + parsedFunctionName: args.functionName, + parsedFunctionArgs: args.args, + until: donePromise, + callbacks: { onChange: (result) => { onDone(); onResult(result); }, }, - ); + }); return resultPromise; } @@ -130,56 +257,73 @@ export function formatValue(value: Value) { export async function subscribeAndLog( ctx: Context, - deploymentUrl: string, - adminKey: string, - functionName: string, - args: Record, - componentPath: string | undefined, + args: { + deploymentUrl: string; + adminKey: string; + functionName: string; + argsString: string; + identityString?: string; + componentPath: string | undefined; + }, ) { - return subscribe( + const { projectConfig } = await readProjectConfig(ctx); + + const parsedFunctionName = await parseFunctionName( ctx, - deploymentUrl, - adminKey, - functionName, - args, - componentPath, - waitForever(), - { + args.functionName, + projectConfig.functions, + ); + const identity = args.identityString + ? await getFakeIdentity(ctx, args.identityString) + : undefined; + const functionArgs = await parseArgs(ctx, args.argsString); + return subscribe(ctx, { + deploymentUrl: args.deploymentUrl, + adminKey: args.adminKey, + identity, + parsedFunctionName, + parsedFunctionArgs: functionArgs, + componentPath: args.componentPath, + until: waitForever(), + callbacks: { onStart() { logFinishedStep( ctx, - `Watching query ${functionName} on ${deploymentUrl}...`, + `Watching query ${args.functionName} on ${args.deploymentUrl}...`, ); }, onChange(result) { logOutput(ctx, formatValue(result)); }, onStop() { - logMessage(ctx, `Closing connection to ${deploymentUrl}...`); + logMessage(ctx, `Closing connection to ${args.deploymentUrl}...`); }, }, - ); + }); } export async function subscribe( ctx: Context, - deploymentUrl: string, - adminKey: string, - functionName: string, - args: Record, - componentPath: string | undefined, - until: Promise, - callbacks?: { - onStart?: () => void; - onChange?: (result: Value) => void; - onStop?: () => void; + args: { + deploymentUrl: string; + adminKey: string; + identity?: UserIdentityAttributes; + parsedFunctionName: string; + parsedFunctionArgs: Record; + componentPath: string | undefined; + until: Promise; + callbacks?: { + onStart?: () => void; + onChange?: (result: Value) => void; + onStop?: () => void; + }; }, ) { const client = new BaseConvexClient( - deploymentUrl, + args.deploymentUrl, (updatedQueries) => { for (const queryToken of updatedQueries) { - callbacks?.onChange?.(client.localQueryResultByToken(queryToken)!); + args.callbacks?.onChange?.(client.localQueryResultByToken(queryToken)!); } }, { @@ -188,12 +332,16 @@ export async function subscribe( unsavedChangesWarning: false, }, ); - client.setAdminAuth(adminKey); - const { unsubscribe } = client.subscribe(functionName, args, { - componentPath, - }); + client.setAdminAuth(args.adminKey, args.identity); + const { unsubscribe } = client.subscribe( + args.parsedFunctionName, + args.parsedFunctionArgs, + { + componentPath: args.componentPath, + }, + ); - callbacks?.onStart?.(); + args.callbacks?.onStart?.(); let done = false; const [donePromise, onDone] = waitUntilCalled(); @@ -206,13 +354,13 @@ export async function subscribe( void client.close(); process.off("SIGINT", sigintListener); onDone(); - callbacks?.onStop?.(); + args.callbacks?.onStop?.(); }; function sigintListener() { stopWatching(); } process.on("SIGINT", sigintListener); - void until.finally(stopWatching); + void args.until.finally(stopWatching); while (!done) { // loops once per day (any large value < 2**31 would work) const oneDay = 24 * 60 * 60 * 1000; diff --git a/npm-packages/convex/src/cli/run.ts b/npm-packages/convex/src/cli/run.ts index 40cf8869..f4661311 100644 --- a/npm-packages/convex/src/cli/run.ts +++ b/npm-packages/convex/src/cli/run.ts @@ -25,6 +25,12 @@ export const run = new Command("run") "Watch a query, printing its result if the underlying data changes. Given function must be a query.", ) .option("--push", "Push code to deployment before running the function.") + .addOption( + new Option( + "--identity ", + 'JSON-formatted UserIdentity object, e.g. \'{ name: "John", address: "0x123" }\'', + ), + ) // For backwards compatibility we still support --no-push which is a noop .addOption(new Option("--no-push").hideHelp()) .addDeploymentSelectionOptions(actionDescription("Run the function on")) @@ -70,8 +76,6 @@ export const run = new Command("run") await ensureHasConvexDependency(ctx, "run"); - const args = argsString ? JSON.parse(argsString) : {}; - if (deploymentType === "prod" && options.push) { return await ctx.crash({ exitCode: 1, @@ -105,21 +109,21 @@ export const run = new Command("run") } if (options.watch) { - return await subscribeAndLog( - ctx, + return await subscribeAndLog(ctx, { deploymentUrl, adminKey, functionName, - args, - options.component, - ); + argsString: argsString ?? "{}", + componentPath: options.component, + identityString: options.identity, + }); } - return await runFunctionAndLog( - ctx, + return await runFunctionAndLog(ctx, { deploymentUrl, adminKey, functionName, - args, - options.component, - ); + argsString: argsString ?? "{}", + componentPath: options.component, + identityString: options.identity, + }); });