Skip to content

Commit 1e29d44

Browse files
jordanhunt22Convex, Inc.
authored and
Convex, Inc.
committed
[Expose Function Metadata] Working prototype for function metadata CLI command (#28235)
This is a rough prototype, I am very open to any suggestions. Adds an API that can be used to expose function metadata. This is exposed in the form of a system udf. You can access the results through the CLI (`npx convex api-spec`) . Two assumptions made in this PR are that: - `any` is the default args validator value - `any` is the default return validator value We may decide that we only want to return functions that have both args and returns validators. GitOrigin-RevId: 381916295dc8361f19bb36397da7dc5935833a66
1 parent db344ef commit 1e29d44

File tree

7 files changed

+110
-3
lines changed

7 files changed

+110
-3
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { logOutput, oneoffContext } from "../bundler/context.js";
2+
import {
3+
deploymentSelectionFromOptions,
4+
fetchDeploymentCredentialsWithinCurrentProject,
5+
} from "./lib/api.js";
6+
import { runQuery } from "./lib/run.js";
7+
import { Command } from "@commander-js/extra-typings";
8+
import { actionDescription } from "./lib/command.js";
9+
10+
export const apiSpec = new Command("api-spec")
11+
.summary("List function metadata from your deployment")
12+
.description(
13+
"List argument and return values to your Convex functions.\n\n" +
14+
"By default, this inspects your dev deployment.",
15+
)
16+
.addDeploymentSelectionOptions(
17+
actionDescription("Read function metadata from"),
18+
)
19+
.showHelpAfterError()
20+
.action(async (options) => {
21+
const ctx = oneoffContext;
22+
const deploymentSelection = deploymentSelectionFromOptions(options);
23+
24+
const { adminKey, url: deploymentUrl } =
25+
await fetchDeploymentCredentialsWithinCurrentProject(
26+
ctx,
27+
deploymentSelection,
28+
);
29+
30+
const functions = (await runQuery(
31+
ctx,
32+
deploymentUrl,
33+
adminKey,
34+
"_system/cli/modules:apiSpec",
35+
{},
36+
)) as any[];
37+
38+
logOutput(ctx, JSON.stringify(functions, null, 2));
39+
});

npm-packages/convex/src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { data } from "./data.js";
2929
import inquirer from "inquirer";
3030
import inquirerSearchList from "inquirer-search-list";
3131
import { format } from "util";
32+
import { apiSpec } from "./apiSpec.js";
3233

3334
const MINIMUM_MAJOR_VERSION = 16;
3435
const MINIMUM_MINOR_VERSION = 15;
@@ -120,6 +121,7 @@ async function main() {
120121
.addCommand(update)
121122
.addCommand(logout)
122123
.addCommand(networkTest, { hidden: true })
124+
.addCommand(apiSpec, { hidden: true })
123125
.addHelpCommand("help <command>", "Show help for given <command>")
124126
.version(version)
125127
// Hide version and help so they don't clutter

npm-packages/system-udfs/convex/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
FunctionReference,
1616
} from "convex/server";
1717
import type * as _system_cli_exports from "../_system/cli/exports.js";
18+
import type * as _system_cli_modules from "../_system/cli/modules.js";
1819
import type * as _system_cli_queryEnvironmentVariables from "../_system/cli/queryEnvironmentVariables.js";
1920
import type * as _system_cli_queryImport from "../_system/cli/queryImport.js";
2021
import type * as _system_cli_queryTable from "../_system/cli/queryTable.js";
@@ -69,6 +70,7 @@ import type * as tableDefs_snapshotImport from "../tableDefs/snapshotImport.js";
6970
*/
7071
declare const fullApi: ApiFromModules<{
7172
"_system/cli/exports": typeof _system_cli_exports;
73+
"_system/cli/modules": typeof _system_cli_modules;
7274
"_system/cli/queryEnvironmentVariables": typeof _system_cli_queryEnvironmentVariables;
7375
"_system/cli/queryImport": typeof _system_cli_queryImport;
7476
"_system/cli/queryTable": typeof _system_cli_queryTable;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
These UDFs are not used by the dashboard, but they are used by the CLI.
22

3-
They get deployed with a backend push, just like the \_system/frontend ones.
3+
They get deployed with a funrun push, just like the \_system/frontend ones.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ValidatorJSON } from "../../../../convex/dist/internal-cjs-types/values";
2+
import { UdfType, Visibility } from "../frontend/common";
3+
import { queryPrivateSystem } from "../secretSystemTables";
4+
import { v } from "convex/values";
5+
6+
type FunctionSpec = {
7+
identifier: string;
8+
function_type: UdfType;
9+
visibility: Visibility;
10+
args?: ValidatorJSON;
11+
returns?: ValidatorJSON;
12+
};
13+
14+
type HttpFunctionSpec = {
15+
function_type: "HttpAction";
16+
method: string;
17+
path: string;
18+
};
19+
20+
type FunctionSpecs = (FunctionSpec | HttpFunctionSpec)[];
21+
22+
export const DEFAULT_ARGS_VALIDATOR = '{ "type": "any" }';
23+
export const DEFAULT_RETURN_VALIDATOR = '{ "type": "any" }';
24+
25+
export const apiSpec = queryPrivateSystem({
26+
args: {
27+
componentId: v.optional(v.union(v.string(), v.null())),
28+
},
29+
handler: async ({ db }): Promise<FunctionSpecs> => {
30+
const result: FunctionSpecs = [];
31+
for await (const module of db.query("_modules")) {
32+
const analyzeResult = module.analyzeResult;
33+
if (!analyzeResult) {
34+
// `Skipping ${module.path}`
35+
continue;
36+
}
37+
38+
for (const fn of analyzeResult.sourceMapped?.functions || []) {
39+
result.push({
40+
identifier: module.path + ":" + fn.name,
41+
function_type: fn.udfType,
42+
visibility: fn.visibility ?? { kind: "public" },
43+
args: JSON.parse(fn.args ?? DEFAULT_ARGS_VALIDATOR),
44+
returns:
45+
JSON.parse(fn.returns ?? DEFAULT_RETURN_VALIDATOR) ??
46+
JSON.parse(DEFAULT_RETURN_VALIDATOR),
47+
});
48+
}
49+
50+
for (const httpFn of analyzeResult.sourceMapped?.httpRoutes || []) {
51+
result.push({
52+
function_type: "HttpAction",
53+
method: httpFn.route.method,
54+
path: httpFn.route.path,
55+
});
56+
}
57+
}
58+
59+
return result;
60+
},
61+
});

npm-packages/system-udfs/convex/_system/frontend/modules.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "./common";
88
import { queryPrivateSystem } from "../secretSystemTables";
99
import { v } from "convex/values";
10+
import { DEFAULT_ARGS_VALIDATOR } from "../cli/modules";
1011

1112
/**
1213
* Return all user defined modules + their functions.
@@ -73,7 +74,7 @@ function processHttpRoute(f: {
7374
lineno: lineno ? Number(lineno) : undefined,
7475
udfType: "HttpAction",
7576
visibility: { kind: "public" },
76-
argsValidator: f.args || '{ "type": "any" }',
77+
argsValidator: f.args || DEFAULT_ARGS_VALIDATOR,
7778
} as const;
7879
}
7980

@@ -90,6 +91,6 @@ function processFunction(f: {
9091
...f,
9192
lineno: lineno ? Number(lineno) : undefined,
9293
visibility: f.visibility ?? { kind: "public" },
93-
argsValidator: f.args || '{ "type": "any" }',
94+
argsValidator: f.args || DEFAULT_ARGS_VALIDATOR,
9495
};
9596
}

npm-packages/system-udfs/convex/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const analyzedFunction = v.object({
4646
pos: v.optional(analyzedSourcePosition),
4747
udfType,
4848
visibility: v.optional(v.union(v.null(), udfVisibility)),
49+
args: v.optional(v.string()),
50+
returns: v.union(v.string(), v.null()),
4951
});
5052

5153
const analyzedHttpRoute = v.object({

0 commit comments

Comments
 (0)