History

Add Convex OpenAPI route and method-aware functions

Rhys Sullivan committed 9 months ago

Commit 7e6277a1 parent 6da096021 files · +974 −28Browse files

Unified · Split

21 files
  1. executor/packages/database/convex/_generated/api.d.ts +18 −1
  2. executor/packages/database/convex/accounts.ts +1 −0
  3. executor/packages/database/convex/app.ts +4 −3
  4. executor/packages/database/convex/auth.ts +4 −2
  5. executor/packages/database/convex/billing.ts +7 −4
  6. executor/packages/database/convex/credentialsNode.ts +3 −2
  7. executor/packages/database/convex/executor.ts +5 −3
  8. executor/packages/database/convex/executorNode.ts +4 −2
  9. executor/packages/database/convex/http.ts +6 −0
  10. executor/packages/database/convex/http/openapi_handler.ts +339 −0
  11. executor/packages/database/convex/http/openapi_spec.test.ts +97 −0
  12. executor/packages/database/convex/http/openapi_spec.ts +282 −0
  13. executor/packages/database/convex/http/openapi_spec_registry.ts +103 −0
  14. executor/packages/database/convex/invites.ts +3 −0
  15. executor/packages/database/convex/organizationMembers.ts +4 −0
  16. executor/packages/database/convex/organizations.ts +5 −0
  17. executor/packages/database/convex/runtimeCallbacks.ts +7 −4
  18. executor/packages/database/convex/schema.ts +3 −1
  19. executor/packages/database/convex/toolRegistry.ts +63 −3
  20. executor/packages/database/convex/workspace.ts +13 −3
  21. executor/packages/database/convex/workspaces.ts +3 −0
executor/packages/database/convex/_generated/api.d.ts+18−1
@@ -1161,12 +1161,18 @@ export declare const internal: { · 1160 unmodified lines
11611161 { buildId: string; cursor?: string; workspaceId: Id<"workspaces"> },
11621162 any
11631163 >;
1164− finishBuild: FunctionReference<
1164+ failBuild: FunctionReference<
11651165 "mutation",
11661166 "internal",
11671167 { buildId: string; workspaceId: Id<"workspaces"> },
11681168 any
11691169 >;
1170+ finishBuild: FunctionReference<
1171+ "mutation",
1172+ "internal",
1173+ { buildId: string; signature: string; workspaceId: Id<"workspaces"> },
1174+ any
1175+ >;
11701176 getState: FunctionReference<
11711177 "query",
11721178 "internal",
@@ -1274,6 +1280,17 @@ export declare const internal: { · 101 unmodified lines
12741280 },
12751281 any
12761282 >;
1283+ updateBuildMetadata: FunctionReference<
1284+ "mutation",
1285+ "internal",
1286+ {
1287+ buildId: string;
1288+ typesStorageId?: Id<"_storage">;
1289+ warnings: Array<string>;
1290+ workspaceId: Id<"workspaces">;
1291+ },
1292+ any
1293+ >;
12771294 };
12781295 workspaceAuthInternal: {
12791296 getWorkspaceAccessForAnonymousSubject: FunctionReference<
executor/packages/database/convex/accounts.ts+1−0
@@ -2,6 +2,7 @@ import { authedMutation } from "../../core/src/function-builders"; · 1 unmodified line
22 import { deleteCurrentAccountHandler } from "../src/accounts/delete-current-account";
33
44 export const deleteCurrentAccount = authedMutation({
5+ method: "POST",
56 args: {},
67 handler: deleteCurrentAccountHandler,
78 });
executor/packages/database/convex/app.ts+4−3
@@ -1,9 +1,9 @@
1−import { query } from "./_generated/server";
2−import { optionalAccountQuery } from "../../core/src/function-builders";
1+import { customQuery, optionalAccountQuery } from "../../core/src/function-builders";
32
43 const workosEnabled = Boolean(process.env.WORKOS_CLIENT_ID && process.env.WORKOS_API_KEY);
54
6−export const getClientConfig = query({
5+export const getClientConfig = customQuery({
6+ method: "GET",
77 args: {},
88 handler: async () => {
99 return {
@@ -19,6 +19,7 @@ export const getClientConfig = query({ · 9 unmodified lines
1919 });
2020
2121 export const getCurrentAccount = optionalAccountQuery({
22+ method: "GET",
2223 args: {},
2324 handler: async (ctx) => ctx.account,
2425 });
executor/packages/database/convex/auth.ts+4−2
@@ -1,7 +1,8 @@
11 import { AuthKit, type AuthFunctions } from "@convex-dev/workos-authkit";
22 import { components, internal } from "./_generated/api";
33 import type { DataModel } from "./_generated/dataModel.d.ts";
4−import { internalMutation, mutation } from "./_generated/server";
4+import { internalMutation } from "./_generated/server";
5+import { customMutation } from "../../core/src/function-builders";
56 import { bootstrapCurrentWorkosAccountImpl } from "../src/auth/bootstrap";
67 import { workosEventHandlers } from "../src/auth/event_handlers";
78
@@ -39,7 +40,8 @@ export const authKitEvent = authKitEvents?.authKitEvent ?? internalMutation({ · 31 unmodified lines
3940 handler: async () => null,
4041 });
4142
42−export const bootstrapCurrentWorkosAccount = mutation({
43+export const bootstrapCurrentWorkosAccount = customMutation({
44+ method: "POST",
4345 args: {},
4446 handler: bootstrapCurrentWorkosAccountImpl,
4547 });
executor/packages/database/convex/billing.ts+7−4
@@ -1,8 +1,7 @@
11 import { StripeSubscriptions } from "@convex-dev/stripe";
22 import { v } from "convex/values";
33 import { components, internal } from "./_generated/api";
4−import { action } from "./_generated/server";
5−import { organizationMutation, organizationQuery } from "../../core/src/function-builders";
4+import { customAction, organizationMutation, organizationQuery } from "../../core/src/function-builders";
65 import {
76 createCustomerPortalHandler,
87 createSubscriptionCheckoutHandler,
@@ -13,13 +12,15 @@ import { · 4 unmodified lines
1312 const stripeClient = new StripeSubscriptions(components.stripe, {});
1413
1514 export const getSummary = organizationQuery({
15+ method: "GET",
1616 args: {},
1717 handler: async (ctx) => {
1818 return await getBillingSummaryHandler(ctx, components);
1919 },
2020 });
2121
22−export const createSubscriptionCheckout = action({
22+export const createSubscriptionCheckout = customAction({
23+ method: "POST",
2324 args: {
2425 organizationId: v.id("organizations"),
2526 priceId: v.string(),
@@ -32,7 +33,8 @@ export const createSubscriptionCheckout = action({ · 6 unmodified lines
3233 },
3334 });
3435
35−export const createCustomerPortal = action({
36+export const createCustomerPortal = customAction({
37+ method: "POST",
3638 args: {
3739 organizationId: v.id("organizations"),
3840 returnUrl: v.optional(v.string()),
@@ -47,6 +49,7 @@ export const createCustomerPortal = action({ · 8 unmodified lines
4749 });
4850
4951 export const retrySeatSync = organizationMutation({
52+ method: "POST",
5053 requireBillingAdmin: true,
5154 args: {},
5255 handler: async (ctx) => {
executor/packages/database/convex/credentialsNode.ts+3−2
@@ -2,7 +2,7 @@ · 1 unmodified line
22
33 import { v } from "convex/values";
44 import { internal } from "./_generated/api";
5−import { action } from "./_generated/server";
5+import { customAction } from "../../core/src/function-builders";
66 import { upsertCredentialHandler } from "../src/credentials-node/upsert-credential";
77 import {
88 credentialProviderValidator,
@@ -10,7 +10,8 @@ import { · 1 unmodified line
1010 jsonObjectValidator,
1111 } from "../src/database/validators";
1212
13−export const upsertCredential = action({
13+export const upsertCredential = customAction({
14+ method: "POST",
1415 args: {
1516 id: v.optional(v.string()),
1617 workspaceId: v.id("workspaces"),
executor/packages/database/convex/executor.ts+5−3
@@ -1,7 +1,7 @@
11 import { v } from "convex/values";
22 import { internal } from "./_generated/api";
3−import { action, internalMutation } from "./_generated/server";
4−import { workspaceMutation } from "../../core/src/function-builders";
3+import { internalMutation } from "./_generated/server";
4+import { customAction, workspaceMutation } from "../../core/src/function-builders";
55 import {
66 completeRuntimeRunHandler,
77 createTaskHandler,
@@ -11,7 +11,8 @@ import { · 3 unmodified lines
1111 } from "../src/executor/handlers";
1212 import { jsonObjectValidator } from "../src/database/validators";
1313
14−export const createTask = action({
14+export const createTask = customAction({
15+ method: "POST",
1516 args: {
1617 code: v.string(),
1718 timeoutMs: v.optional(v.number()),
@@ -45,6 +46,7 @@ export const createTaskInternal = internalMutation({ · 27 unmodified lines
4546 });
4647
4748 export const resolveApproval = workspaceMutation({
49+ method: "POST",
4850 args: {
4951 approvalId: v.string(),
5052 decision: v.union(v.literal("approved"), v.literal("denied")),
executor/packages/database/convex/executorNode.ts+4−2
@@ -1,7 +1,7 @@
11 "use node";
22
33 import { v } from "convex/values";
4−import { action, internalAction } from "./_generated/server";
4+import { internalAction } from "./_generated/server";
55 import type {
66 ToolCallResult,
77 ToolDescriptor,
@@ -18,8 +18,10 @@ import { · 10 unmodified lines
1818 import { runQueuedTask } from "../src/runtime/task_runner";
1919 import { handleExternalToolCallRequest } from "../src/runtime/external_tool_call";
2020 import { jsonObjectValidator } from "../src/database/validators";
21+import { customAction } from "../../core/src/function-builders";
2122
22−export const listToolsWithWarnings = action({
23+export const listToolsWithWarnings = customAction({
24+ method: "POST",
2325 args: {
2426 workspaceId: v.id("workspaces"),
2527 accountId: v.optional(v.id("accounts")),
executor/packages/database/convex/http.ts+6−0
@@ -4,6 +4,7 @@ import { components } from "./_generated/api"; · 3 unmodified lines
44 import { authKit } from "./auth";
55 import { anonymousJwksHandler, anonymousTokenHandler } from "./http/anonymous_auth";
66 import { mcpAnonymousHandler, mcpHandler } from "./http/mcp_handler";
7+import { openApiHandler } from "./http/openapi_handler";
78 import {
89 oauthAuthorizationServerHandler,
910 oauthProtectedResourceHandler,
@@ -29,4 +30,9 @@ http.route({ path: "/.well-known/jwks.json", method: "GET", handler: anonymousJw · 19 unmodified lines
2930 http.route({ path: "/auth/anonymous/token", method: "POST", handler: anonymousTokenHandler });
3031 http.route({ path: "/auth/anonymous/token", method: "GET", handler: anonymousTokenHandler });
3132
33+http.route({ path: "/api", method: "POST", handler: openApiHandler });
34+http.route({ path: "/api", method: "GET", handler: openApiHandler });
35+http.route({ pathPrefix: "/api/", method: "POST", handler: openApiHandler });
36+http.route({ pathPrefix: "/api/", method: "GET", handler: openApiHandler });
37+
3238 export default http;
executor/packages/database/convex/http/openapi_handler.ts+339−0
@@ -0,0 +1,339 @@
1+import { makeFunctionReference } from "convex/server";
2+import { httpAction } from "../_generated/server";
3+import { buildOpenApiDocument } from "./openapi_spec";
4+import { collectPublicFunctionSpecs } from "./openapi_spec_registry";
5+
6+type FunctionType = "query" | "mutation" | "action";
7+
8+type OpenApiPayload = {
9+ path?: string;
10+ function?: string;
11+ args?: unknown;
12+ format?: string;
13+ type?: string;
14+ [key: string]: unknown;
15+};
16+
17+type HttpActionCtx = Parameters<Parameters<typeof httpAction>[0]>[0];
18+
19+type ActionError = Error & {
20+ data?: unknown;
21+};
22+
23+const JSON_FORMAT = "json";
24+const OPENAPI_SPEC_PATH = "/api/openapi.json";
25+const API_RUN_PREFIX = "/api/run/";
26+const API_PREFIX = "/api/";
27+const CONTROL_QUERY_PARAMS = new Set(["format", "path", "function", "type", "args"]);
28+
29+function parseRunPathToFunctionIdentifier(rawPath: string): string {
30+ const normalizedPath = rawPath.replace(/^\/+|\/+$/g, "");
31+ if (!normalizedPath) {
32+ throw new Error("Function path is required");
33+ }
34+
35+ if (normalizedPath.includes(":")) {
36+ const [modulePathRaw, exportNameRaw] = decodeURIComponent(normalizedPath).split(":");
37+ if (!modulePathRaw) {
38+ throw new Error("Function path is required");
39+ }
40+
41+ const modulePath = modulePathRaw.endsWith(".ts")
42+ ? modulePathRaw.slice(0, -3)
43+ : modulePathRaw;
44+
45+ if (!exportNameRaw || exportNameRaw === "default") {
46+ return modulePath;
47+ }
48+
49+ return `${modulePath}:${exportNameRaw}`;
50+ }
51+
52+ if (normalizedPath.endsWith(".ts")) {
53+ return decodeURIComponent(normalizedPath.slice(0, -3));
54+ }
55+
56+ const segments = normalizedPath.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
57+ if (segments.length === 0) {
58+ throw new Error("Function path is required");
59+ }
60+
61+ if (segments.length === 1) {
62+ return segments[0]!;
63+ }
64+
65+ const exportName = segments[segments.length - 1]!;
66+ const modulePath = segments.slice(0, -1).join("/");
67+ return exportName === "default" ? modulePath : `${modulePath}:${exportName}`;
68+}
69+
70+function functionIdentifierFromUrlPath(pathname: string): string | undefined {
71+ if (pathname.startsWith(API_RUN_PREFIX)) {
72+ return parseRunPathToFunctionIdentifier(pathname.slice(API_RUN_PREFIX.length));
73+ }
74+
75+ if (pathname === "/api" || pathname === "/api/") {
76+ return undefined;
77+ }
78+
79+ if (pathname.startsWith(API_PREFIX)) {
80+ return parseRunPathToFunctionIdentifier(pathname.slice(API_PREFIX.length));
81+ }
82+
83+ return undefined;
84+}
85+
86+function toActionError(error: unknown): ActionError {
87+ if (error instanceof Error) {
88+ return error as ActionError;
89+ }
90+ return new Error("Unknown function error");
91+}
92+
93+function isTypeLookupError(error: unknown, type: FunctionType): boolean {
94+ const message = toActionError(error).message;
95+ return message.startsWith(`No ${type} named`)
96+ || message.startsWith("Could not find public function")
97+ || message.startsWith("Could not find function")
98+ || message.includes(`is not a ${type}`)
99+ || message.includes("Function not found");
100+}
101+
102+function isLookupError(error: unknown): boolean {
103+ const message = toActionError(error).message;
104+ return message.startsWith("No query named")
105+ || message.startsWith("No mutation named")
106+ || message.startsWith("No action named")
107+ || message.startsWith("Could not find public function")
108+ || message.startsWith("Could not find function")
109+ || message.includes("is not a query")
110+ || message.includes("is not a mutation")
111+ || message.includes("is not an action")
112+ || message.includes("Function not found");
113+}
114+
115+function errorResponse(errorMessage: string, status = 400): Response {
116+ return Response.json(
117+ {
118+ errorMessage,
119+ },
120+ { status },
121+ );
122+}
123+
124+function functionErrorResponse(error: unknown, status?: number): Response {
125+ const actionError = toActionError(error);
126+ const resolvedStatus = status ?? (isLookupError(actionError) ? 404 : 500);
127+
128+ return Response.json(
129+ {
130+ errorMessage: actionError.message,
131+ ...(actionError.data === undefined ? {} : { errorData: actionError.data }),
132+ },
133+ { status: resolvedStatus },
134+ );
135+}
136+
137+function successResponse(value: unknown): Response {
138+ return Response.json(value === undefined ? null : value, { status: 200 });
139+}
140+
141+function isRecord(value: unknown): value is Record<string, unknown> {
142+ return typeof value === "object" && value !== null && !Array.isArray(value);
143+}
144+
145+function isFunctionType(value: unknown): value is FunctionType {
146+ return value === "query" || value === "mutation" || value === "action";
147+}
148+
149+function parseQueryValue(rawValue: string): unknown {
150+ const value = rawValue.trim();
151+ const looksJson = value.startsWith("{")
152+ || value.startsWith("[")
153+ || value === "true"
154+ || value === "false"
155+ || value === "null"
156+ || /^-?\d+(?:\.\d+)?$/.test(value)
157+ || /^".*"$/.test(value);
158+
159+ if (looksJson) {
160+ try {
161+ return JSON.parse(value);
162+ } catch {
163+ return rawValue;
164+ }
165+ }
166+
167+ return rawValue;
168+}
169+
170+function mergeQueryParam(args: Record<string, unknown>, key: string, value: unknown): void {
171+ const existing = args[key];
172+ if (existing === undefined) {
173+ args[key] = value;
174+ return;
175+ }
176+
177+ if (Array.isArray(existing)) {
178+ existing.push(value);
179+ return;
180+ }
181+
182+ args[key] = [existing, value];
183+}
184+
185+async function parsePostPayload(request: Request): Promise<OpenApiPayload> {
186+ const rawBody = await request.text();
187+ if (rawBody.trim().length === 0) {
188+ return {};
189+ }
190+
191+ const parsed = JSON.parse(rawBody) as unknown;
192+ if (!isRecord(parsed)) {
193+ throw new Error("Request body must be a JSON object");
194+ }
195+
196+ return parsed as OpenApiPayload;
197+}
198+
199+function parseGetPayload(url: URL): OpenApiPayload {
200+ const format = url.searchParams.get("format") ?? undefined;
201+ const path = url.searchParams.get("path") ?? undefined;
202+ const type = url.searchParams.get("type") ?? undefined;
203+ const inlineArgs: Record<string, unknown> = {};
204+
205+ for (const [key, rawValue] of url.searchParams.entries()) {
206+ if (CONTROL_QUERY_PARAMS.has(key)) {
207+ continue;
208+ }
209+ mergeQueryParam(inlineArgs, key, parseQueryValue(rawValue));
210+ }
211+
212+ const argsParam = url.searchParams.get("args");
213+ let args: Record<string, unknown> | undefined;
214+ if (argsParam) {
215+ const parsedArgs = JSON.parse(argsParam) as unknown;
216+ if (!isRecord(parsedArgs)) {
217+ throw new Error("args query parameter must be a JSON object");
218+ }
219+ args = {
220+ ...parsedArgs,
221+ ...inlineArgs,
222+ };
223+ } else if (Object.keys(inlineArgs).length > 0) {
224+ args = inlineArgs;
225+ }
226+
227+ return {
228+ ...(type ? { type } : {}),
229+ ...(format ? { format } : {}),
230+ ...(path ? { path } : {}),
231+ ...(args ? { args } : {}),
232+ };
233+}
234+
235+function extractArgs(payload: OpenApiPayload): Record<string, unknown> {
236+ if (payload.args !== undefined) {
237+ if (!isRecord(payload.args)) {
238+ throw new Error("args must be an object");
239+ }
240+ return payload.args;
241+ }
242+
243+ const directArgs: Record<string, unknown> = {};
244+ for (const [key, value] of Object.entries(payload)) {
245+ if (CONTROL_QUERY_PARAMS.has(key)) {
246+ continue;
247+ }
248+ directArgs[key] = value;
249+ }
250+ return directArgs;
251+}
252+
253+async function runAsType(
254+ ctx: HttpActionCtx,
255+ type: FunctionType,
256+ functionIdentifier: string,
257+ args: Record<string, unknown>,
258+): Promise<unknown> {
259+ switch (type) {
260+ case "query":
261+ return await ctx.runQuery(makeFunctionReference<"query">(functionIdentifier), args);
262+ case "mutation":
263+ return await ctx.runMutation(makeFunctionReference<"mutation">(functionIdentifier), args);
264+ case "action":
265+ return await ctx.runAction(makeFunctionReference<"action">(functionIdentifier), args);
266+ }
267+}
268+
269+export const openApiHandler = httpAction(async (ctx, request) => {
270+ try {
271+ const url = new URL(request.url);
272+
273+ if (request.method === "GET" && url.pathname === OPENAPI_SPEC_PATH) {
274+ const functions = await collectPublicFunctionSpecs();
275+ return Response.json(buildOpenApiDocument(functions, url.origin), {
276+ status: 200,
277+ headers: {
278+ "content-type": "application/json; charset=utf-8",
279+ },
280+ });
281+ }
282+
283+ const urlIdentifier = functionIdentifierFromUrlPath(url.pathname);
284+
285+ const payload = request.method === "GET"
286+ ? parseGetPayload(url)
287+ : await parsePostPayload(request);
288+
289+ const payloadIdentifier = payload.path ?? payload.function;
290+ const functionIdentifier = urlIdentifier
291+ ?? (typeof payloadIdentifier === "string" ? parseRunPathToFunctionIdentifier(payloadIdentifier) : undefined);
292+
293+ if (!functionIdentifier) {
294+ return errorResponse("Function path is required. Provide /api/run/<function> or a body path field.");
295+ }
296+
297+ const format = payload.format ?? JSON_FORMAT;
298+ if (format !== JSON_FORMAT) {
299+ return errorResponse(`Only format=\"${JSON_FORMAT}\" is supported`);
300+ }
301+
302+ const args = extractArgs(payload);
303+
304+ if (payload.type && !isFunctionType(payload.type)) {
305+ return errorResponse("type must be one of: query, mutation, action");
306+ }
307+
308+ if (payload.type && isFunctionType(payload.type)) {
309+ try {
310+ const value = await runAsType(ctx, payload.type, functionIdentifier, args);
311+ return successResponse(value);
312+ } catch (error) {
313+ return functionErrorResponse(error, isTypeLookupError(error, payload.type) ? 404 : 500);
314+ }
315+ }
316+
317+ const attemptedTypes: FunctionType[] = request.method === "GET"
318+ ? ["query"]
319+ : ["query", "mutation", "action"];
320+
321+ let lookupError: unknown;
322+ for (const type of attemptedTypes) {
323+ try {
324+ const value = await runAsType(ctx, type, functionIdentifier, args);
325+ return successResponse(value);
326+ } catch (error) {
327+ if (isTypeLookupError(error, type)) {
328+ lookupError = error;
329+ continue;
330+ }
331+ return functionErrorResponse(error, 500);
332+ }
333+ }
334+
335+ return functionErrorResponse(lookupError ?? new Error(`Function not found: ${functionIdentifier}`), 404);
336+ } catch (error) {
337+ return errorResponse(error instanceof Error ? error.message : "Invalid OpenAPI request payload");
338+ }
339+});
executor/packages/database/convex/http/openapi_spec.test.ts+97−0
@@ -0,0 +1,97 @@
1+import { describe, expect, test } from "bun:test";
2+import { buildOpenApiDocument, schemaFromValidator, type OpenApiFunctionSpec } from "./openapi_spec";
3+import { collectPublicFunctionSpecs } from "./openapi_spec_registry";
4+
5+describe("openapi_spec", () => {
6+ test("builds object schemas with required fields", () => {
7+ const schema = schemaFromValidator({
8+ type: "object",
9+ value: {
10+ workspaceId: {
11+ fieldType: { type: "id", tableName: "workspaces" },
12+ optional: false,
13+ },
14+ sessionId: {
15+ fieldType: { type: "string" },
16+ optional: true,
17+ },
18+ },
19+ });
20+
21+ expect(schema).toEqual({
22+ type: "object",
23+ required: ["workspaceId"],
24+ properties: {
25+ workspaceId: {
26+ type: "string",
27+ description: 'ID from table "workspaces"',
28+ },
29+ sessionId: {
30+ type: "string",
31+ },
32+ },
33+ });
34+ });
35+
36+ test("parses stringified validator json", () => {
37+ const schema = schemaFromValidator(
38+ '{"type":"object","value":{"name":{"fieldType":{"type":"string"},"optional":false}}}',
39+ );
40+
41+ expect(schema).toEqual({
42+ type: "object",
43+ required: ["name"],
44+ properties: {
45+ name: {
46+ type: "string",
47+ },
48+ },
49+ });
50+ });
51+
52+ test("generates GET and POST operations from function specs", () => {
53+ const functions: OpenApiFunctionSpec[] = [
54+ {
55+ identifier: "app:getClientConfig",
56+ functionType: "query",
57+ method: "GET",
58+ args: { type: "object", value: {} },
59+ returns: { type: "object", value: {} },
60+ },
61+ {
62+ identifier: "workspace:upsertCredential",
63+ functionType: "mutation",
64+ method: "POST",
65+ args: { type: "object", value: {} },
66+ returns: { type: "any" },
67+ },
68+ ];
69+
70+ const document = buildOpenApiDocument(functions, "https://example.convex.cloud");
71+ const paths = document.paths as Record<string, Record<string, unknown>>;
72+
73+ expect(paths["/api/run/app/getClientConfig"]?.get).toBeDefined();
74+ expect(paths["/api/run/workspace/upsertCredential"]?.post).toBeDefined();
75+ });
76+
77+ test("collects public function specs from convex modules", async () => {
78+ const specs = await collectPublicFunctionSpecs();
79+ const identifiers = new Set(specs.map((spec) => spec.identifier));
80+
81+ expect(identifiers.has("app:getClientConfig")).toBe(true);
82+ expect(identifiers.has("workspace:listTasks")).toBe(true);
83+ expect(identifiers.has("executor:createTask")).toBe(true);
84+
85+ const updateRole = specs.find((spec) => spec.identifier === "organizationMembers:updateRole");
86+ expect(typeof updateRole?.args).toBe("string");
87+
88+ const updateRoleDoc = buildOpenApiDocument([updateRole!], "https://example.convex.cloud");
89+ const schemas = (updateRoleDoc.components as { schemas: Record<string, unknown> }).schemas;
90+ const argsSchema = schemas.Request_organizationMembers_updateRole as {
91+ properties?: Record<string, unknown>;
92+ };
93+
94+ expect(argsSchema.properties?.accountId).toBeDefined();
95+ expect(argsSchema.properties?.role).toBeDefined();
96+ });
97+});
executor/packages/database/convex/http/openapi_spec.ts+282−0
@@ -0,0 +1,282 @@
1+type FunctionType = "query" | "mutation" | "action";
2+
3+const CONTROL_QUERY_PARAMS = new Set(["format", "args", "path", "function", "type"]);
4+
5+export type ValidatorJson = {
6+ type: string;
7+ [key: string]: unknown;
8+};
9+
10+export type OpenApiFunctionSpec = {
11+ identifier: string;
12+ functionType: FunctionType;
13+ method: "GET" | "POST";
14+ args?: ValidatorJson | string | null;
15+ returns?: ValidatorJson | string | null;
16+};
17+
18+function toRecord(value: unknown): Record<string, unknown> {
19+ return typeof value === "object" && value !== null && !Array.isArray(value)
20+ ? (value as Record<string, unknown>)
21+ : {};
22+}
23+
24+function schemaId(identifier: string): string {
25+ return identifier.replace(/[^a-zA-Z0-9_]/g, "_");
26+}
27+
28+function endpointPath(identifier: string): string {
29+ const [modulePath, exportName] = identifier.split(":");
30+ if (!modulePath || !exportName) {
31+ return `/api/run/${identifier.replace(/:/g, "/")}`;
32+ }
33+ return `/api/run/${modulePath}/${exportName}`;
34+}
35+
36+function buildGetParametersFromArgsSchema(argsSchema: Record<string, unknown>): Array<Record<string, unknown>> {
37+ const schemaType = typeof argsSchema.type === "string" ? argsSchema.type : undefined;
38+ const properties = toRecord(argsSchema.properties);
39+ const required = Array.isArray(argsSchema.required)
40+ ? new Set(argsSchema.required.filter((entry): entry is string => typeof entry === "string"))
41+ : new Set<string>();
42+
43+ if (schemaType !== "object" || Object.keys(properties).length === 0) {
44+ return [{
45+ in: "query",
46+ name: "args",
47+ required: false,
48+ schema: {
49+ type: "string",
50+ description: "JSON-encoded args object",
51+ },
52+ }];
53+ }
54+
55+ const parameters: Array<Record<string, unknown>> = [];
56+ for (const [name, value] of Object.entries(properties)) {
57+ if (CONTROL_QUERY_PARAMS.has(name)) {
58+ continue;
59+ }
60+
61+ parameters.push({
62+ in: "query",
63+ name,
64+ required: required.has(name),
65+ schema: toRecord(value),
66+ });
67+ }
68+
69+ return parameters;
70+}
71+
72+export function schemaFromValidator(validator?: ValidatorJson | string | null): Record<string, unknown> {
73+ if (typeof validator === "string") {
74+ try {
75+ const parsed = JSON.parse(validator) as ValidatorJson;
76+ return schemaFromValidator(parsed);
77+ } catch {
78+ return {};
79+ }
80+ }
81+
82+ if (!validator || typeof validator !== "object") {
83+ return {};
84+ }
85+
86+ switch (validator.type) {
87+ case "null":
88+ return { type: "string", nullable: true };
89+ case "number":
90+ return { type: "number" };
91+ case "bigint":
92+ return { type: "integer", format: "int64" };
93+ case "boolean":
94+ return { type: "boolean" };
95+ case "string":
96+ return { type: "string" };
97+ case "bytes":
98+ return { type: "string", format: "byte" };
99+ case "any":
100+ return {};
101+ case "literal": {
102+ const literalValue = validator.value;
103+ if (typeof literalValue === "string") {
104+ return { type: "string", enum: [literalValue] };
105+ }
106+ if (typeof literalValue === "boolean") {
107+ return { type: "boolean", enum: [literalValue] };
108+ }
109+ return { type: "number", enum: [literalValue] };
110+ }
111+ case "id":
112+ return {
113+ type: "string",
114+ description: `ID from table "${String(validator.tableName ?? "unknown")}"`,
115+ };
116+ case "array":
117+ return {
118+ type: "array",
119+ items: schemaFromValidator((validator.value as ValidatorJson | undefined) ?? { type: "any" }),
120+ };
121+ case "record": {
122+ const values = validator.values as { fieldType?: ValidatorJson } | undefined;
123+ return {
124+ type: "object",
125+ additionalProperties: schemaFromValidator(values?.fieldType ?? { type: "any" }),
126+ };
127+ }
128+ case "object": {
129+ const rawMembers = validator.value as Record<string, { fieldType: ValidatorJson; optional?: boolean }> | undefined;
130+ const members = rawMembers ?? {};
131+ const properties: Record<string, unknown> = {};
132+ const required: string[] = [];
133+
134+ for (const [key, value] of Object.entries(members)) {
135+ properties[key] = schemaFromValidator(value.fieldType);
136+ if (!value.optional) {
137+ required.push(key);
138+ }
139+ }
140+
141+ return {
142+ type: "object",
143+ ...(required.length > 0 ? { required } : {}),
144+ ...(Object.keys(properties).length > 0 ? { properties } : {}),
145+ };
146+ }
147+ case "union": {
148+ const members = Array.isArray(validator.value) ? (validator.value as ValidatorJson[]) : [];
149+ const hasNull = members.some((member) => member.type === "null");
150+ const nonNullMembers = members.filter((member) => member.type !== "null");
151+
152+ if (nonNullMembers.length === 1) {
153+ return {
154+ ...schemaFromValidator(nonNullMembers[0]),
155+ ...(hasNull ? { nullable: true } : {}),
156+ };
157+ }
158+
159+ return {
160+ ...(hasNull ? { nullable: true } : {}),
161+ oneOf: nonNullMembers.map((member) => schemaFromValidator(member)),
162+ };
163+ }
164+ default:
165+ return {};
166+ }
167+}
168+
169+export function buildOpenApiDocument(functions: OpenApiFunctionSpec[], serverUrl: string): Record<string, unknown> {
170+ const paths: Record<string, Record<string, unknown>> = {};
171+ const schemas: Record<string, unknown> = {
172+ FailedResponse: {
173+ type: "object",
174+ properties: {
175+ errorMessage: { type: "string" },
176+ errorData: {},
177+ },
178+ },
179+ };
180+
181+ for (const fn of functions) {
182+ const id = schemaId(fn.identifier);
183+ const requestSchemaName = `Request_${id}`;
184+ const responseSchemaName = `Response_${id}`;
185+
186+ const argsSchema = schemaFromValidator(fn.args);
187+ schemas[requestSchemaName] = argsSchema;
188+ schemas[responseSchemaName] = schemaFromValidator(fn.returns);
189+
190+ const path = endpointPath(fn.identifier);
191+ const method = fn.method.toLowerCase();
192+
193+ const baseOperation: Record<string, unknown> = {
194+ summary: `Calls ${fn.functionType} ${fn.identifier}`,
195+ tags: [fn.functionType],
196+ operationId: `${fn.functionType}_${id}`,
197+ responses: {
198+ "200": {
199+ description: "Convex executed the request",
200+ content: {
201+ "application/json": {
202+ schema: { $ref: `#/components/schemas/${responseSchemaName}` },
203+ },
204+ },
205+ },
206+ "400": {
207+ description: "Invalid request payload",
208+ content: {
209+ "application/json": {
210+ schema: { $ref: "#/components/schemas/FailedResponse" },
211+ },
212+ },
213+ },
214+ "404": {
215+ description: "Function not found",
216+ content: {
217+ "application/json": {
218+ schema: { $ref: "#/components/schemas/FailedResponse" },
219+ },
220+ },
221+ },
222+ "500": {
223+ description: "Function execution failed",
224+ content: {
225+ "application/json": {
226+ schema: { $ref: "#/components/schemas/FailedResponse" },
227+ },
228+ },
229+ },
230+ },
231+ };
232+
233+ const operation = fn.method === "POST"
234+ ? {
235+ ...baseOperation,
236+ requestBody: {
237+ required: true,
238+ content: {
239+ "application/json": {
240+ schema: { $ref: `#/components/schemas/${requestSchemaName}` },
241+ },
242+ },
243+ },
244+ }
245+ : {
246+ ...baseOperation,
247+ parameters: buildGetParametersFromArgsSchema(argsSchema),
248+ };
249+
250+ const existingPath = paths[path] ?? {};
251+ paths[path] = {
252+ ...existingPath,
253+ [method]: operation,
254+ };
255+ }
256+
257+ return {
258+ openapi: "3.0.3",
259+ info: {
260+ title: "Convex API",
261+ version: "0.0.0",
262+ },
263+ servers: [{ url: serverUrl }],
264+ security: [{ bearerAuth: [] }],
265+ tags: [
266+ { name: "query", description: "Functions that read data" },
267+ { name: "mutation", description: "Functions that write data" },
268+ { name: "action", description: "Functions that can call external APIs" },
269+ ],
270+ paths,
271+ components: {
272+ securitySchemes: {
273+ bearerAuth: {
274+ type: "http",
275+ scheme: "bearer",
276+ description: "Use Authorization: Bearer <token>",
277+ },
278+ },
279+ schemas,
280+ },
281+ };
282+}
executor/packages/database/convex/http/openapi_spec_registry.ts+103−0
@@ -0,0 +1,103 @@
1+import type { OpenApiFunctionSpec, ValidatorJson } from "./openapi_spec";
2+
3+type ConvexPublicRegistration = {
4+ isPublic?: boolean;
5+ isQuery?: boolean;
6+ isMutation?: boolean;
7+ isAction?: boolean;
8+ exportArgs?: () => ValidatorJson | null;
9+ exportReturns?: () => ValidatorJson | null;
10+};
11+
12+function readValidator(
13+ fn: ConvexPublicRegistration,
14+ key: "exportArgs" | "exportReturns",
15+): ValidatorJson | string | null {
16+ try {
17+ return fn[key]?.() ?? null;
18+ } catch {
19+ return null;
20+ }
21+}
22+
23+function collectFromModule(modulePath: string, exportsObject: Record<string, unknown>): OpenApiFunctionSpec[] {
24+ const functions: OpenApiFunctionSpec[] = [];
25+
26+ for (const [exportName, value] of Object.entries(exportsObject)) {
27+ if (typeof value !== "function") {
28+ continue;
29+ }
30+
31+ const fn = value as ConvexPublicRegistration;
32+ if (!fn.isPublic) {
33+ continue;
34+ }
35+
36+ const functionType = fn.isQuery ? "query" : fn.isMutation ? "mutation" : fn.isAction ? "action" : null;
37+ if (!functionType) {
38+ continue;
39+ }
40+
41+ functions.push({
42+ identifier: `${modulePath}:${exportName}`,
43+ functionType,
44+ method: functionType === "query" ? "GET" : "POST",
45+ args: readValidator(fn, "exportArgs"),
46+ returns: readValidator(fn, "exportReturns"),
47+ });
48+ }
49+
50+ return functions;
51+}
52+
53+const moduleLoaders: Array<{
54+ modulePath: string;
55+ load: () => Promise<Record<string, unknown>>;
56+}> = [
57+ { modulePath: "accounts", load: () => import("../accounts") },
58+ { modulePath: "app", load: () => import("../app") },
59+ { modulePath: "auth", load: () => import("../auth") },
60+ { modulePath: "billing", load: () => import("../billing") },
61+ { modulePath: "executor", load: () => import("../executor") },
62+ { modulePath: "invites", load: () => import("../invites") },
63+ { modulePath: "organizationMembers", load: () => import("../organizationMembers") },
64+ { modulePath: "organizations", load: () => import("../organizations") },
65+ { modulePath: "runtimeCallbacks", load: () => import("../runtimeCallbacks") },
66+ { modulePath: "workspace", load: () => import("../workspace") },
67+ { modulePath: "workspaces", load: () => import("../workspaces") },
68+];
69+
70+export async function collectPublicFunctionSpecs(): Promise<OpenApiFunctionSpec[]> {
71+ const moduleFunctions: OpenApiFunctionSpec[] = [];
72+
73+ for (const loader of moduleLoaders) {
74+ try {
75+ const exportsObject = await loader.load();
76+ moduleFunctions.push(...collectFromModule(loader.modulePath, exportsObject));
77+ } catch {
78+ // Skip modules that are unavailable in the current runtime.
79+ }
80+ }
81+
82+ // Node runtime public actions are declared in "use node" files and are added manually.
83+ const nodeRuntimeFunctions: OpenApiFunctionSpec[] = [
84+ {
85+ identifier: "credentialsNode:upsertCredential",
86+ functionType: "action",
87+ method: "POST",
88+ args: { type: "any" },
89+ returns: { type: "any" },
90+ },
91+ {
92+ identifier: "executorNode:listToolsWithWarnings",
93+ functionType: "action",
94+ method: "POST",
95+ args: { type: "any" },
96+ returns: { type: "any" },
97+ },
98+ ];
99+
100+ const merged = [...moduleFunctions, ...nodeRuntimeFunctions];
101+ merged.sort((a, b) => a.identifier.localeCompare(b.identifier));
102+ return merged;
103+}
executor/packages/database/convex/invites.ts+3−0
@@ -23,6 +23,7 @@ const organizationRoleValidator = v.union( · 22 unmodified lines
2323 );
2424
2525 export const list = organizationQuery({
26+ method: "GET",
2627 requireAdmin: true,
2728 args: {},
2829 handler: async (ctx) => {
@@ -47,6 +48,7 @@ export const list = organizationQuery({ · 18 unmodified lines
4748 });
4849
4950 export const create = organizationMutation({
51+ method: "POST",
5052 requireAdmin: true,
5153 args: {
5254 email: v.string(),
@@ -197,6 +199,7 @@ export const deliverWorkosInvite = internalAction({ · 144 unmodified lines
197199 });
198200
199201 export const revoke = organizationMutation({
202+ method: "POST",
200203 requireAdmin: true,
201204 args: {
202205 inviteId: v.id("invites"),
executor/packages/database/convex/organizationMembers.ts+4−0
@@ -13,6 +13,7 @@ const organizationRoleValidator = v.union( · 12 unmodified lines
1313 );
1414
1515 export const list = organizationQuery({
16+ method: "GET",
1617 args: {},
1718 handler: async (ctx) => {
1819 const members = await ctx.db
@@ -43,6 +44,7 @@ export const list = organizationQuery({ · 24 unmodified lines
4344 });
4445
4546 export const updateRole = organizationMutation({
47+ method: "POST",
4648 args: {
4749 accountId: v.id("accounts"),
4850 role: organizationRoleValidator,
@@ -70,6 +72,7 @@ export const updateRole = organizationMutation({ · 21 unmodified lines
7072 });
7173
7274 export const updateBillable = organizationMutation({
75+ method: "POST",
7376 args: {
7477 accountId: v.id("accounts"),
7578 billable: v.boolean(),
@@ -99,6 +102,7 @@ export const updateBillable = organizationMutation({ · 23 unmodified lines
99102 });
100103
101104 export const remove = organizationMutation({
105+ method: "POST",
102106 args: {
103107 accountId: v.id("accounts"),
104108 },
executor/packages/database/convex/organizations.ts+5−0
@@ -9,6 +9,7 @@ import { · 8 unmodified lines
99 } from "../src/organizations/handlers";
1010
1111 export const create = authedMutation({
12+ method: "POST",
1213 args: {
1314 name: v.string(),
1415 },
@@ -18,6 +19,7 @@ export const create = authedMutation({ · 3 unmodified lines
1819 });
1920
2021 export const listMine = optionalAccountQuery({
22+ method: "GET",
2123 args: {},
2224 handler: async (ctx) => {
2325 return await listOrganizationsMineHandler(ctx);
@@ -25,6 +27,7 @@ export const listMine = optionalAccountQuery({ · 1 unmodified line
2527 });
2628
2729 export const getNavigationState = optionalAccountQuery({
30+ method: "GET",
2831 args: {},
2932 handler: async (ctx) => {
3033 return await getNavigationStateHandler(ctx);
@@ -32,6 +35,7 @@ export const getNavigationState = optionalAccountQuery({ · 1 unmodified line
3235 });
3336
3437 export const getOrganizationAccess = optionalAccountQuery({
38+ method: "GET",
3539 args: {
3640 organizationId: v.id("organizations"),
3741 },
@@ -41,6 +45,7 @@ export const getOrganizationAccess = optionalAccountQuery({ · 3 unmodified lines
4145 });
4246
4347 export const resolveWorkosOrganizationId = optionalAccountQuery({
48+ method: "GET",
4449 args: {
4550 organizationId: v.id("organizations"),
4651 },
executor/packages/database/convex/runtimeCallbacks.ts+7−4
@@ -1,6 +1,6 @@
11 import { v } from "convex/values";
22 import { internal } from "./_generated/api";
3−import { action, mutation, query } from "./_generated/server";
3+import { customAction, customMutation, customQuery } from "../../core/src/function-builders";
44 import {
55 completeRunHandler,
66 getApprovalStatusHandler,
@@ -8,7 +8,8 @@ import { · 1 unmodified line
88 } from "../src/runtime-callbacks/handlers";
99 import { jsonObjectValidator } from "../src/database/validators";
1010
11−export const handleToolCall = action({
11+export const handleToolCall = customAction({
12+ method: "POST",
1213 args: {
1314 internalSecret: v.string(),
1415 runId: v.string(),
@@ -21,7 +22,8 @@ export const handleToolCall = action({ · 6 unmodified lines
2122 },
2223 });
2324
24−export const completeRun = mutation({
25+export const completeRun = customMutation({
26+ method: "POST",
2527 args: {
2628 internalSecret: v.string(),
2729 runId: v.string(),
@@ -35,7 +37,8 @@ export const completeRun = mutation({ · 7 unmodified lines
3537 },
3638 });
3739
38−export const getApprovalStatus = query({
40+export const getApprovalStatus = customQuery({
41+ method: "GET",
3942 args: {
4043 internalSecret: v.string(),
4144 runId: v.string(),
executor/packages/database/convex/schema.ts+3−1
@@ -451,9 +451,11 @@ export default defineSchema({ · 450 unmodified lines
451451 // Stores the currently "ready" build id for search + invocation.
452452 workspaceToolRegistryState: defineTable({
453453 workspaceId: v.id("workspaces"),
454− signature: v.string(),
454+ signature: v.optional(v.string()),
455455 readyBuildId: v.optional(v.string()),
456456 buildingBuildId: v.optional(v.string()),
457+ typesStorageId: v.optional(v.id("_storage")),
458+ warnings: v.optional(v.array(v.string())),
457459 createdAt: v.number(),
458460 updatedAt: v.number(),
459461 })
executor/packages/database/convex/toolRegistry.ts+63−3
@@ -18,6 +18,8 @@ export const getState = internalQuery({ · 17 unmodified lines
1818 signature: entry.signature,
1919 readyBuildId: entry.readyBuildId,
2020 buildingBuildId: entry.buildingBuildId,
21+ typesStorageId: entry.typesStorageId,
22+ warnings: entry.warnings ?? [],
2123 updatedAt: entry.updatedAt,
2224 };
2325 },
@@ -38,7 +40,6 @@ export const beginBuild = internalMutation({ · 14 unmodified lines
3840
3941 if (existing) {
4042 await ctx.db.patch(existing._id, {
41− signature: args.signature,
4243 buildingBuildId: args.buildId,
4344 updatedAt: now,
4445 });
@@ -50,6 +51,8 @@ export const beginBuild = internalMutation({ · 5 unmodified lines
5051 signature: args.signature,
5152 readyBuildId: undefined,
5253 buildingBuildId: args.buildId,
54+ typesStorageId: undefined,
55+ warnings: [],
5356 createdAt: now,
5457 updatedAt: now,
5558 });
@@ -144,6 +147,7 @@ export const finishBuild = internalMutation({ · 88 unmodified lines
144147 args: {
145148 workspaceId: v.id("workspaces"),
146149 buildId: v.string(),
150+ signature: v.string(),
147151 },
148152 handler: async (ctx, args) => {
149153 const now = Date.now();
@@ -155,9 +159,11 @@ export const finishBuild = internalMutation({ · 5 unmodified lines
155159 if (!state) {
156160 await ctx.db.insert("workspaceToolRegistryState", {
157161 workspaceId: args.workspaceId,
158− signature: "",
162+ signature: args.signature,
159163 readyBuildId: args.buildId,
160164 buildingBuildId: undefined,
165+ typesStorageId: undefined,
166+ warnings: [],
161167 createdAt: now,
162168 updatedAt: now,
163169 });
@@ -171,12 +177,64 @@ export const finishBuild = internalMutation({ · 7 unmodified lines
171177
172178 await ctx.db.patch(state._id, {
173179 readyBuildId: args.buildId,
180+ signature: args.signature,
174181 buildingBuildId: undefined,
175182 updatedAt: now,
176183 });
177184 },
178185 });
179186
187+export const updateBuildMetadata = internalMutation({
188+ args: {
189+ workspaceId: v.id("workspaces"),
190+ buildId: v.string(),
191+ typesStorageId: v.optional(v.id("_storage")),
192+ warnings: v.array(v.string()),
193+ },
194+ handler: async (ctx, args) => {
195+ const state = await ctx.db
196+ .query("workspaceToolRegistryState")
197+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
198+ .unique();
199+ if (!state) {
200+ return;
201+ }
202+ if (state.readyBuildId !== args.buildId) {
203+ return;
204+ }
205+
206+ await ctx.db.patch(state._id, {
207+ typesStorageId: args.typesStorageId,
208+ warnings: args.warnings,
209+ updatedAt: Date.now(),
210+ });
211+ },
212+});
213+
214+export const failBuild = internalMutation({
215+ args: {
216+ workspaceId: v.id("workspaces"),
217+ buildId: v.string(),
218+ },
219+ handler: async (ctx, args) => {
220+ const state = await ctx.db
221+ .query("workspaceToolRegistryState")
222+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
223+ .unique();
224+ if (!state) {
225+ return;
226+ }
227+ if (state.buildingBuildId !== args.buildId) {
228+ return;
229+ }
230+
231+ await ctx.db.patch(state._id, {
232+ buildingBuildId: undefined,
233+ updatedAt: Date.now(),
234+ });
235+ },
236+});
237+
180238 const PRUNE_DELETE_PAGE_SIZE = 10;
181239 const PRUNE_NAMESPACE_SCAN_PAGE_SIZE = 250;
182240
@@ -363,6 +421,7 @@ export const getToolByPath = internalQuery({ · 180 unmodified lines
363421 displayOutput: entry.displayOutput,
364422 requiredInputKeys: entry.requiredInputKeys,
365423 previewInputKeys: entry.previewInputKeys,
424+ typedRef: entry.typedRef,
366425 serializedToolJson: entry.serializedToolJson,
367426 };
368427 },
@@ -377,7 +436,7 @@ export const listToolsByNamespace = internalQuery({ · 8 unmodified lines
377436 },
378437 handler: async (ctx, args) => {
379438 const namespace = args.namespace.trim().toLowerCase();
380− const limit = Math.max(1, Math.min(200, Math.floor(args.limit)));
439+ const limit = Math.max(1, Math.min(20_000, Math.floor(args.limit)));
381440 if (!namespace) return [];
382441
383442 const entries = await ctx.db
@@ -398,6 +457,7 @@ export const listToolsByNamespace = internalQuery({ · 14 unmodified lines
398457 displayOutput: entry.displayOutput,
399458 requiredInputKeys: entry.requiredInputKeys,
400459 previewInputKeys: entry.previewInputKeys,
460+ typedRef: entry.typedRef,
401461 }));
402462 },
403463 });
executor/packages/database/convex/workspace.ts+13−3
@@ -1,7 +1,6 @@
11 import { v } from "convex/values";
22 import { internal } from "./_generated/api";
3−import { mutation } from "./_generated/server";
4−import { workspaceMutation, workspaceQuery } from "../../core/src/function-builders";
3+import { customMutation, workspaceMutation, workspaceQuery } from "../../core/src/function-builders";
54 import { getOrganizationMembership, isAdminRole } from "../../core/src/identity";
65 import {
76 credentialProviderValidator,
@@ -15,7 +14,8 @@ import { · 7 unmodified lines
1514 toolSourceTypeValidator,
1615 } from "../src/database/validators";
1716
18−export const bootstrapAnonymousSession = mutation({
17+export const bootstrapAnonymousSession = customMutation({
18+ method: "POST",
1919 args: {
2020 sessionId: v.optional(v.string()),
2121 accountId: v.optional(v.string()),
@@ -27,6 +27,7 @@ export const bootstrapAnonymousSession = mutation({ · 5 unmodified lines
2727 });
2828
2929 export const listTasks = workspaceQuery({
30+ method: "GET",
3031 args: {},
3132 handler: async (ctx) => {
3233 return await ctx.runQuery(internal.database.listTasks, {
@@ -36,6 +37,7 @@ export const listTasks = workspaceQuery({ · 3 unmodified lines
3637 });
3738
3839 export const listPendingApprovals = workspaceQuery({
40+ method: "GET",
3941 args: {},
4042 handler: async (ctx) => {
4143 return await ctx.runQuery(internal.database.listPendingApprovals, {
@@ -45,6 +47,7 @@ export const listPendingApprovals = workspaceQuery({ · 3 unmodified lines
4547 });
4648
4749 export const upsertAccessPolicy = workspaceMutation({
50+ method: "POST",
4851 requireAdmin: true,
4952 args: {
5053 id: v.optional(v.string()),
@@ -73,6 +76,7 @@ export const upsertAccessPolicy = workspaceMutation({ · 22 unmodified lines
7376 });
7477
7578 export const listAccessPolicies = workspaceQuery({
79+ method: "GET",
7680 args: {},
7781 handler: async (ctx) => {
7882 return await ctx.runQuery(internal.database.listAccessPolicies, {
@@ -83,6 +87,7 @@ export const listAccessPolicies = workspaceQuery({ · 4 unmodified lines
8387 });
8488
8589 export const upsertCredential = workspaceMutation({
90+ method: "POST",
8691 requireAdmin: true,
8792 args: {
8893 id: v.optional(v.string()),
@@ -109,6 +114,7 @@ export const upsertCredential = workspaceMutation({ · 20 unmodified lines
109114 });
110115
111116 export const listCredentials = workspaceQuery({
117+ method: "GET",
112118 requireAdmin: true,
113119 args: {},
114120 handler: async (ctx) => {
@@ -128,6 +134,7 @@ export const listCredentials = workspaceQuery({ · 13 unmodified lines
128134 });
129135
130136 export const resolveCredential = workspaceQuery({
137+ method: "GET",
131138 requireAdmin: true,
132139 args: {
133140 sourceKey: v.string(),
@@ -143,6 +150,7 @@ export const resolveCredential = workspaceQuery({ · 9 unmodified lines
143150 });
144151
145152 export const upsertToolSource = workspaceMutation({
153+ method: "POST",
146154 requireAdmin: true,
147155 args: {
148156 id: v.optional(v.string()),
@@ -168,6 +176,7 @@ export const upsertToolSource = workspaceMutation({ · 19 unmodified lines
168176 });
169177
170178 export const listToolSources = workspaceQuery({
179+ method: "GET",
171180 args: {},
172181 handler: async (ctx) => {
173182 return await ctx.runQuery(internal.database.listToolSources, {
@@ -177,6 +186,7 @@ export const listToolSources = workspaceQuery({ · 3 unmodified lines
177186 });
178187
179188 export const deleteToolSource = workspaceMutation({
189+ method: "POST",
180190 requireAdmin: true,
181191 args: {
182192 sourceId: v.string(),
executor/packages/database/convex/workspaces.ts+3−0
@@ -7,6 +7,7 @@ import { · 6 unmodified lines
77 } from "../src/workspaces/handlers";
88
99 export const create = authedMutation({
10+ method: "POST",
1011 args: {
1112 name: v.string(),
1213 organizationId: v.optional(v.id("organizations")),
@@ -18,6 +19,7 @@ export const create = authedMutation({ · 5 unmodified lines
1819 });
1920
2021 export const list = optionalAccountQuery({
22+ method: "GET",
2123 args: {
2224 organizationId: v.optional(v.id("organizations")),
2325 },
@@ -27,6 +29,7 @@ export const list = optionalAccountQuery({ · 3 unmodified lines
2729 });
2830
2931 export const generateWorkspaceIconUploadUrl = authedMutation({
32+ method: "POST",
3033 args: {},
3134 handler: async (ctx) => {
3235 return await generateWorkspaceIconUploadUrlHandler(ctx);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t