Unified · Split
| @@ -1161,12 +1161,18 @@ export declare const internal: { · 1160 unmodified lines | |||
| 1161 | { buildId: string; cursor?: string; workspaceId: Id<"workspaces"> }, | 1161 | { buildId: string; cursor?: string; workspaceId: Id<"workspaces"> }, |
| 1162 | any | 1162 | any |
| 1163 | >; | 1163 | >; |
| 1164 | finishBuild: FunctionReference< | 1164 | failBuild: FunctionReference< |
| 1165 | "mutation", | 1165 | "mutation", |
| 1166 | "internal", | 1166 | "internal", |
| 1167 | { buildId: string; workspaceId: Id<"workspaces"> }, | 1167 | { buildId: string; workspaceId: Id<"workspaces"> }, |
| 1168 | any | 1168 | any |
| 1169 | >; | 1169 | >; |
| 1170 | finishBuild: FunctionReference< | ||
| 1171 | "mutation", | ||
| 1172 | "internal", | ||
| 1173 | { buildId: string; signature: string; workspaceId: Id<"workspaces"> }, | ||
| 1174 | any | ||
| 1175 | >; | ||
| 1170 | getState: FunctionReference< | 1176 | getState: FunctionReference< |
| 1171 | "query", | 1177 | "query", |
| 1172 | "internal", | 1178 | "internal", |
| @@ -1274,6 +1280,17 @@ export declare const internal: { · 101 unmodified lines | |||
| 1274 | }, | 1280 | }, |
| 1275 | any | 1281 | any |
| 1276 | >; | 1282 | >; |
| 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 | >; | ||
| 1277 | }; | 1294 | }; |
| 1278 | workspaceAuthInternal: { | 1295 | workspaceAuthInternal: { |
| 1279 | getWorkspaceAccessForAnonymousSubject: FunctionReference< | 1296 | getWorkspaceAccessForAnonymousSubject: FunctionReference< |
| @@ -2,6 +2,7 @@ import { authedMutation } from "../../core/src/function-builders"; · 1 unmodified line | |||
| 2 | import { deleteCurrentAccountHandler } from "../src/accounts/delete-current-account"; | 2 | import { deleteCurrentAccountHandler } from "../src/accounts/delete-current-account"; |
| 3 | 3 | ||
| 4 | export const deleteCurrentAccount = authedMutation({ | 4 | export const deleteCurrentAccount = authedMutation({ |
| 5 | method: "POST", | ||
| 5 | args: {}, | 6 | args: {}, |
| 6 | handler: deleteCurrentAccountHandler, | 7 | handler: deleteCurrentAccountHandler, |
| 7 | }); | 8 | }); |
| @@ -1,9 +1,9 @@ | |||
| 1 | import { query } from "./_generated/server"; | 1 | import { customQuery, optionalAccountQuery } from "../../core/src/function-builders"; |
| 2 | import { optionalAccountQuery } from "../../core/src/function-builders"; | ||
| 3 | 2 | ||
| 4 | const workosEnabled = Boolean(process.env.WORKOS_CLIENT_ID && process.env.WORKOS_API_KEY); | 3 | const workosEnabled = Boolean(process.env.WORKOS_CLIENT_ID && process.env.WORKOS_API_KEY); |
| 5 | 4 | ||
| 6 | export const getClientConfig = query({ | 5 | export const getClientConfig = customQuery({ |
| 6 | method: "GET", | ||
| 7 | args: {}, | 7 | args: {}, |
| 8 | handler: async () => { | 8 | handler: async () => { |
| 9 | return { | 9 | return { |
| @@ -19,6 +19,7 @@ export const getClientConfig = query({ · 9 unmodified lines | |||
| 19 | }); | 19 | }); |
| 20 | 20 | ||
| 21 | export const getCurrentAccount = optionalAccountQuery({ | 21 | export const getCurrentAccount = optionalAccountQuery({ |
| 22 | method: "GET", | ||
| 22 | args: {}, | 23 | args: {}, |
| 23 | handler: async (ctx) => ctx.account, | 24 | handler: async (ctx) => ctx.account, |
| 24 | }); | 25 | }); |
| @@ -1,7 +1,8 @@ | |||
| 1 | import { AuthKit, type AuthFunctions } from "@convex-dev/workos-authkit"; | 1 | import { AuthKit, type AuthFunctions } from "@convex-dev/workos-authkit"; |
| 2 | import { components, internal } from "./_generated/api"; | 2 | import { components, internal } from "./_generated/api"; |
| 3 | import type { DataModel } from "./_generated/dataModel.d.ts"; | 3 | 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"; | ||
| 5 | import { bootstrapCurrentWorkosAccountImpl } from "../src/auth/bootstrap"; | 6 | import { bootstrapCurrentWorkosAccountImpl } from "../src/auth/bootstrap"; |
| 6 | import { workosEventHandlers } from "../src/auth/event_handlers"; | 7 | import { workosEventHandlers } from "../src/auth/event_handlers"; |
| 7 | 8 | ||
| @@ -39,7 +40,8 @@ export const authKitEvent = authKitEvents?.authKitEvent ?? internalMutation({ · 31 unmodified lines | |||
| 39 | handler: async () => null, | 40 | handler: async () => null, |
| 40 | }); | 41 | }); |
| 41 | 42 | ||
| 42 | export const bootstrapCurrentWorkosAccount = mutation({ | 43 | export const bootstrapCurrentWorkosAccount = customMutation({ |
| 44 | method: "POST", | ||
| 43 | args: {}, | 45 | args: {}, |
| 44 | handler: bootstrapCurrentWorkosAccountImpl, | 46 | handler: bootstrapCurrentWorkosAccountImpl, |
| 45 | }); | 47 | }); |
| @@ -1,8 +1,7 @@ | |||
| 1 | import { StripeSubscriptions } from "@convex-dev/stripe"; | 1 | import { StripeSubscriptions } from "@convex-dev/stripe"; |
| 2 | import { v } from "convex/values"; | 2 | import { v } from "convex/values"; |
| 3 | import { components, internal } from "./_generated/api"; | 3 | import { components, internal } from "./_generated/api"; |
| 4 | import { action } from "./_generated/server"; | 4 | import { customAction, organizationMutation, organizationQuery } from "../../core/src/function-builders"; |
| 5 | import { organizationMutation, organizationQuery } from "../../core/src/function-builders"; | ||
| 6 | import { | 5 | import { |
| 7 | createCustomerPortalHandler, | 6 | createCustomerPortalHandler, |
| 8 | createSubscriptionCheckoutHandler, | 7 | createSubscriptionCheckoutHandler, |
| @@ -13,13 +12,15 @@ import { · 4 unmodified lines | |||
| 13 | const stripeClient = new StripeSubscriptions(components.stripe, {}); | 12 | const stripeClient = new StripeSubscriptions(components.stripe, {}); |
| 14 | 13 | ||
| 15 | export const getSummary = organizationQuery({ | 14 | export const getSummary = organizationQuery({ |
| 15 | method: "GET", | ||
| 16 | args: {}, | 16 | args: {}, |
| 17 | handler: async (ctx) => { | 17 | handler: async (ctx) => { |
| 18 | return await getBillingSummaryHandler(ctx, components); | 18 | return await getBillingSummaryHandler(ctx, components); |
| 19 | }, | 19 | }, |
| 20 | }); | 20 | }); |
| 21 | 21 | ||
| 22 | export const createSubscriptionCheckout = action({ | 22 | export const createSubscriptionCheckout = customAction({ |
| 23 | method: "POST", | ||
| 23 | args: { | 24 | args: { |
| 24 | organizationId: v.id("organizations"), | 25 | organizationId: v.id("organizations"), |
| 25 | priceId: v.string(), | 26 | priceId: v.string(), |
| @@ -32,7 +33,8 @@ export const createSubscriptionCheckout = action({ · 6 unmodified lines | |||
| 32 | }, | 33 | }, |
| 33 | }); | 34 | }); |
| 34 | 35 | ||
| 35 | export const createCustomerPortal = action({ | 36 | export const createCustomerPortal = customAction({ |
| 37 | method: "POST", | ||
| 36 | args: { | 38 | args: { |
| 37 | organizationId: v.id("organizations"), | 39 | organizationId: v.id("organizations"), |
| 38 | returnUrl: v.optional(v.string()), | 40 | returnUrl: v.optional(v.string()), |
| @@ -47,6 +49,7 @@ export const createCustomerPortal = action({ · 8 unmodified lines | |||
| 47 | }); | 49 | }); |
| 48 | 50 | ||
| 49 | export const retrySeatSync = organizationMutation({ | 51 | export const retrySeatSync = organizationMutation({ |
| 52 | method: "POST", | ||
| 50 | requireBillingAdmin: true, | 53 | requireBillingAdmin: true, |
| 51 | args: {}, | 54 | args: {}, |
| 52 | handler: async (ctx) => { | 55 | handler: async (ctx) => { |
| @@ -2,7 +2,7 @@ · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import { v } from "convex/values"; | 3 | import { v } from "convex/values"; |
| 4 | import { internal } from "./_generated/api"; | 4 | import { internal } from "./_generated/api"; |
| 5 | import { action } from "./_generated/server"; | 5 | import { customAction } from "../../core/src/function-builders"; |
| 6 | import { upsertCredentialHandler } from "../src/credentials-node/upsert-credential"; | 6 | import { upsertCredentialHandler } from "../src/credentials-node/upsert-credential"; |
| 7 | import { | 7 | import { |
| 8 | credentialProviderValidator, | 8 | credentialProviderValidator, |
| @@ -10,7 +10,8 @@ import { · 1 unmodified line | |||
| 10 | jsonObjectValidator, | 10 | jsonObjectValidator, |
| 11 | } from "../src/database/validators"; | 11 | } from "../src/database/validators"; |
| 12 | 12 | ||
| 13 | export const upsertCredential = action({ | 13 | export const upsertCredential = customAction({ |
| 14 | method: "POST", | ||
| 14 | args: { | 15 | args: { |
| 15 | id: v.optional(v.string()), | 16 | id: v.optional(v.string()), |
| 16 | workspaceId: v.id("workspaces"), | 17 | workspaceId: v.id("workspaces"), |
| @@ -1,7 +1,7 @@ | |||
| 1 | import { v } from "convex/values"; | 1 | import { v } from "convex/values"; |
| 2 | import { internal } from "./_generated/api"; | 2 | import { internal } from "./_generated/api"; |
| 3 | import { action, internalMutation } from "./_generated/server"; | 3 | import { internalMutation } from "./_generated/server"; |
| 4 | import { workspaceMutation } from "../../core/src/function-builders"; | 4 | import { customAction, workspaceMutation } from "../../core/src/function-builders"; |
| 5 | import { | 5 | import { |
| 6 | completeRuntimeRunHandler, | 6 | completeRuntimeRunHandler, |
| 7 | createTaskHandler, | 7 | createTaskHandler, |
| @@ -11,7 +11,8 @@ import { · 3 unmodified lines | |||
| 11 | } from "../src/executor/handlers"; | 11 | } from "../src/executor/handlers"; |
| 12 | import { jsonObjectValidator } from "../src/database/validators"; | 12 | import { jsonObjectValidator } from "../src/database/validators"; |
| 13 | 13 | ||
| 14 | export const createTask = action({ | 14 | export const createTask = customAction({ |
| 15 | method: "POST", | ||
| 15 | args: { | 16 | args: { |
| 16 | code: v.string(), | 17 | code: v.string(), |
| 17 | timeoutMs: v.optional(v.number()), | 18 | timeoutMs: v.optional(v.number()), |
| @@ -45,6 +46,7 @@ export const createTaskInternal = internalMutation({ · 27 unmodified lines | |||
| 45 | }); | 46 | }); |
| 46 | 47 | ||
| 47 | export const resolveApproval = workspaceMutation({ | 48 | export const resolveApproval = workspaceMutation({ |
| 49 | method: "POST", | ||
| 48 | args: { | 50 | args: { |
| 49 | approvalId: v.string(), | 51 | approvalId: v.string(), |
| 50 | decision: v.union(v.literal("approved"), v.literal("denied")), | 52 | decision: v.union(v.literal("approved"), v.literal("denied")), |
| @@ -1,7 +1,7 @@ | |||
| 1 | "use node"; | 1 | "use node"; |
| 2 | 2 | ||
| 3 | import { v } from "convex/values"; | 3 | import { v } from "convex/values"; |
| 4 | import { action, internalAction } from "./_generated/server"; | 4 | import { internalAction } from "./_generated/server"; |
| 5 | import type { | 5 | import type { |
| 6 | ToolCallResult, | 6 | ToolCallResult, |
| 7 | ToolDescriptor, | 7 | ToolDescriptor, |
| @@ -18,8 +18,10 @@ import { · 10 unmodified lines | |||
| 18 | import { runQueuedTask } from "../src/runtime/task_runner"; | 18 | import { runQueuedTask } from "../src/runtime/task_runner"; |
| 19 | import { handleExternalToolCallRequest } from "../src/runtime/external_tool_call"; | 19 | import { handleExternalToolCallRequest } from "../src/runtime/external_tool_call"; |
| 20 | import { jsonObjectValidator } from "../src/database/validators"; | 20 | import { jsonObjectValidator } from "../src/database/validators"; |
| 21 | import { customAction } from "../../core/src/function-builders"; | ||
| 21 | 22 | ||
| 22 | export const listToolsWithWarnings = action({ | 23 | export const listToolsWithWarnings = customAction({ |
| 24 | method: "POST", | ||
| 23 | args: { | 25 | args: { |
| 24 | workspaceId: v.id("workspaces"), | 26 | workspaceId: v.id("workspaces"), |
| 25 | accountId: v.optional(v.id("accounts")), | 27 | accountId: v.optional(v.id("accounts")), |
| @@ -4,6 +4,7 @@ import { components } from "./_generated/api"; · 3 unmodified lines | |||
| 4 | import { authKit } from "./auth"; | 4 | import { authKit } from "./auth"; |
| 5 | import { anonymousJwksHandler, anonymousTokenHandler } from "./http/anonymous_auth"; | 5 | import { anonymousJwksHandler, anonymousTokenHandler } from "./http/anonymous_auth"; |
| 6 | import { mcpAnonymousHandler, mcpHandler } from "./http/mcp_handler"; | 6 | import { mcpAnonymousHandler, mcpHandler } from "./http/mcp_handler"; |
| 7 | import { openApiHandler } from "./http/openapi_handler"; | ||
| 7 | import { | 8 | import { |
| 8 | oauthAuthorizationServerHandler, | 9 | oauthAuthorizationServerHandler, |
| 9 | oauthProtectedResourceHandler, | 10 | oauthProtectedResourceHandler, |
| @@ -29,4 +30,9 @@ http.route({ path: "/.well-known/jwks.json", method: "GET", handler: anonymousJw · 19 unmodified lines | |||
| 29 | http.route({ path: "/auth/anonymous/token", method: "POST", handler: anonymousTokenHandler }); | 30 | http.route({ path: "/auth/anonymous/token", method: "POST", handler: anonymousTokenHandler }); |
| 30 | http.route({ path: "/auth/anonymous/token", method: "GET", handler: anonymousTokenHandler }); | 31 | http.route({ path: "/auth/anonymous/token", method: "GET", handler: anonymousTokenHandler }); |
| 31 | 32 | ||
| 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 | |||
| 32 | export default http; | 38 | export default http; |
| @@ -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 | }); | ||
| @@ -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 | }); | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -23,6 +23,7 @@ const organizationRoleValidator = v.union( · 22 unmodified lines | |||
| 23 | ); | 23 | ); |
| 24 | 24 | ||
| 25 | export const list = organizationQuery({ | 25 | export const list = organizationQuery({ |
| 26 | method: "GET", | ||
| 26 | requireAdmin: true, | 27 | requireAdmin: true, |
| 27 | args: {}, | 28 | args: {}, |
| 28 | handler: async (ctx) => { | 29 | handler: async (ctx) => { |
| @@ -47,6 +48,7 @@ export const list = organizationQuery({ · 18 unmodified lines | |||
| 47 | }); | 48 | }); |
| 48 | 49 | ||
| 49 | export const create = organizationMutation({ | 50 | export const create = organizationMutation({ |
| 51 | method: "POST", | ||
| 50 | requireAdmin: true, | 52 | requireAdmin: true, |
| 51 | args: { | 53 | args: { |
| 52 | email: v.string(), | 54 | email: v.string(), |
| @@ -197,6 +199,7 @@ export const deliverWorkosInvite = internalAction({ · 144 unmodified lines | |||
| 197 | }); | 199 | }); |
| 198 | 200 | ||
| 199 | export const revoke = organizationMutation({ | 201 | export const revoke = organizationMutation({ |
| 202 | method: "POST", | ||
| 200 | requireAdmin: true, | 203 | requireAdmin: true, |
| 201 | args: { | 204 | args: { |
| 202 | inviteId: v.id("invites"), | 205 | inviteId: v.id("invites"), |
| @@ -13,6 +13,7 @@ const organizationRoleValidator = v.union( · 12 unmodified lines | |||
| 13 | ); | 13 | ); |
| 14 | 14 | ||
| 15 | export const list = organizationQuery({ | 15 | export const list = organizationQuery({ |
| 16 | method: "GET", | ||
| 16 | args: {}, | 17 | args: {}, |
| 17 | handler: async (ctx) => { | 18 | handler: async (ctx) => { |
| 18 | const members = await ctx.db | 19 | const members = await ctx.db |
| @@ -43,6 +44,7 @@ export const list = organizationQuery({ · 24 unmodified lines | |||
| 43 | }); | 44 | }); |
| 44 | 45 | ||
| 45 | export const updateRole = organizationMutation({ | 46 | export const updateRole = organizationMutation({ |
| 47 | method: "POST", | ||
| 46 | args: { | 48 | args: { |
| 47 | accountId: v.id("accounts"), | 49 | accountId: v.id("accounts"), |
| 48 | role: organizationRoleValidator, | 50 | role: organizationRoleValidator, |
| @@ -70,6 +72,7 @@ export const updateRole = organizationMutation({ · 21 unmodified lines | |||
| 70 | }); | 72 | }); |
| 71 | 73 | ||
| 72 | export const updateBillable = organizationMutation({ | 74 | export const updateBillable = organizationMutation({ |
| 75 | method: "POST", | ||
| 73 | args: { | 76 | args: { |
| 74 | accountId: v.id("accounts"), | 77 | accountId: v.id("accounts"), |
| 75 | billable: v.boolean(), | 78 | billable: v.boolean(), |
| @@ -99,6 +102,7 @@ export const updateBillable = organizationMutation({ · 23 unmodified lines | |||
| 99 | }); | 102 | }); |
| 100 | 103 | ||
| 101 | export const remove = organizationMutation({ | 104 | export const remove = organizationMutation({ |
| 105 | method: "POST", | ||
| 102 | args: { | 106 | args: { |
| 103 | accountId: v.id("accounts"), | 107 | accountId: v.id("accounts"), |
| 104 | }, | 108 | }, |
| @@ -9,6 +9,7 @@ import { · 8 unmodified lines | |||
| 9 | } from "../src/organizations/handlers"; | 9 | } from "../src/organizations/handlers"; |
| 10 | 10 | ||
| 11 | export const create = authedMutation({ | 11 | export const create = authedMutation({ |
| 12 | method: "POST", | ||
| 12 | args: { | 13 | args: { |
| 13 | name: v.string(), | 14 | name: v.string(), |
| 14 | }, | 15 | }, |
| @@ -18,6 +19,7 @@ export const create = authedMutation({ · 3 unmodified lines | |||
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | export const listMine = optionalAccountQuery({ | 21 | export const listMine = optionalAccountQuery({ |
| 22 | method: "GET", | ||
| 21 | args: {}, | 23 | args: {}, |
| 22 | handler: async (ctx) => { | 24 | handler: async (ctx) => { |
| 23 | return await listOrganizationsMineHandler(ctx); | 25 | return await listOrganizationsMineHandler(ctx); |
| @@ -25,6 +27,7 @@ export const listMine = optionalAccountQuery({ · 1 unmodified line | |||
| 25 | }); | 27 | }); |
| 26 | 28 | ||
| 27 | export const getNavigationState = optionalAccountQuery({ | 29 | export const getNavigationState = optionalAccountQuery({ |
| 30 | method: "GET", | ||
| 28 | args: {}, | 31 | args: {}, |
| 29 | handler: async (ctx) => { | 32 | handler: async (ctx) => { |
| 30 | return await getNavigationStateHandler(ctx); | 33 | return await getNavigationStateHandler(ctx); |
| @@ -32,6 +35,7 @@ export const getNavigationState = optionalAccountQuery({ · 1 unmodified line | |||
| 32 | }); | 35 | }); |
| 33 | 36 | ||
| 34 | export const getOrganizationAccess = optionalAccountQuery({ | 37 | export const getOrganizationAccess = optionalAccountQuery({ |
| 38 | method: "GET", | ||
| 35 | args: { | 39 | args: { |
| 36 | organizationId: v.id("organizations"), | 40 | organizationId: v.id("organizations"), |
| 37 | }, | 41 | }, |
| @@ -41,6 +45,7 @@ export const getOrganizationAccess = optionalAccountQuery({ · 3 unmodified lines | |||
| 41 | }); | 45 | }); |
| 42 | 46 | ||
| 43 | export const resolveWorkosOrganizationId = optionalAccountQuery({ | 47 | export const resolveWorkosOrganizationId = optionalAccountQuery({ |
| 48 | method: "GET", | ||
| 44 | args: { | 49 | args: { |
| 45 | organizationId: v.id("organizations"), | 50 | organizationId: v.id("organizations"), |
| 46 | }, | 51 | }, |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { v } from "convex/values"; | 1 | import { v } from "convex/values"; |
| 2 | import { internal } from "./_generated/api"; | 2 | import { internal } from "./_generated/api"; |
| 3 | import { action, mutation, query } from "./_generated/server"; | 3 | import { customAction, customMutation, customQuery } from "../../core/src/function-builders"; |
| 4 | import { | 4 | import { |
| 5 | completeRunHandler, | 5 | completeRunHandler, |
| 6 | getApprovalStatusHandler, | 6 | getApprovalStatusHandler, |
| @@ -8,7 +8,8 @@ import { · 1 unmodified line | |||
| 8 | } from "../src/runtime-callbacks/handlers"; | 8 | } from "../src/runtime-callbacks/handlers"; |
| 9 | import { jsonObjectValidator } from "../src/database/validators"; | 9 | import { jsonObjectValidator } from "../src/database/validators"; |
| 10 | 10 | ||
| 11 | export const handleToolCall = action({ | 11 | export const handleToolCall = customAction({ |
| 12 | method: "POST", | ||
| 12 | args: { | 13 | args: { |
| 13 | internalSecret: v.string(), | 14 | internalSecret: v.string(), |
| 14 | runId: v.string(), | 15 | runId: v.string(), |
| @@ -21,7 +22,8 @@ export const handleToolCall = action({ · 6 unmodified lines | |||
| 21 | }, | 22 | }, |
| 22 | }); | 23 | }); |
| 23 | 24 | ||
| 24 | export const completeRun = mutation({ | 25 | export const completeRun = customMutation({ |
| 26 | method: "POST", | ||
| 25 | args: { | 27 | args: { |
| 26 | internalSecret: v.string(), | 28 | internalSecret: v.string(), |
| 27 | runId: v.string(), | 29 | runId: v.string(), |
| @@ -35,7 +37,8 @@ export const completeRun = mutation({ · 7 unmodified lines | |||
| 35 | }, | 37 | }, |
| 36 | }); | 38 | }); |
| 37 | 39 | ||
| 38 | export const getApprovalStatus = query({ | 40 | export const getApprovalStatus = customQuery({ |
| 41 | method: "GET", | ||
| 39 | args: { | 42 | args: { |
| 40 | internalSecret: v.string(), | 43 | internalSecret: v.string(), |
| 41 | runId: v.string(), | 44 | runId: v.string(), |
| @@ -451,9 +451,11 @@ export default defineSchema({ · 450 unmodified lines | |||
| 451 | // Stores the currently "ready" build id for search + invocation. | 451 | // Stores the currently "ready" build id for search + invocation. |
| 452 | workspaceToolRegistryState: defineTable({ | 452 | workspaceToolRegistryState: defineTable({ |
| 453 | workspaceId: v.id("workspaces"), | 453 | workspaceId: v.id("workspaces"), |
| 454 | signature: v.string(), | 454 | signature: v.optional(v.string()), |
| 455 | readyBuildId: v.optional(v.string()), | 455 | readyBuildId: v.optional(v.string()), |
| 456 | buildingBuildId: v.optional(v.string()), | 456 | buildingBuildId: v.optional(v.string()), |
| 457 | typesStorageId: v.optional(v.id("_storage")), | ||
| 458 | warnings: v.optional(v.array(v.string())), | ||
| 457 | createdAt: v.number(), | 459 | createdAt: v.number(), |
| 458 | updatedAt: v.number(), | 460 | updatedAt: v.number(), |
| 459 | }) | 461 | }) |
| @@ -18,6 +18,8 @@ export const getState = internalQuery({ · 17 unmodified lines | |||
| 18 | signature: entry.signature, | 18 | signature: entry.signature, |
| 19 | readyBuildId: entry.readyBuildId, | 19 | readyBuildId: entry.readyBuildId, |
| 20 | buildingBuildId: entry.buildingBuildId, | 20 | buildingBuildId: entry.buildingBuildId, |
| 21 | typesStorageId: entry.typesStorageId, | ||
| 22 | warnings: entry.warnings ?? [], | ||
| 21 | updatedAt: entry.updatedAt, | 23 | updatedAt: entry.updatedAt, |
| 22 | }; | 24 | }; |
| 23 | }, | 25 | }, |
| @@ -38,7 +40,6 @@ export const beginBuild = internalMutation({ · 14 unmodified lines | |||
| 38 | 40 | ||
| 39 | if (existing) { | 41 | if (existing) { |
| 40 | await ctx.db.patch(existing._id, { | 42 | await ctx.db.patch(existing._id, { |
| 41 | signature: args.signature, | ||
| 42 | buildingBuildId: args.buildId, | 43 | buildingBuildId: args.buildId, |
| 43 | updatedAt: now, | 44 | updatedAt: now, |
| 44 | }); | 45 | }); |
| @@ -50,6 +51,8 @@ export const beginBuild = internalMutation({ · 5 unmodified lines | |||
| 50 | signature: args.signature, | 51 | signature: args.signature, |
| 51 | readyBuildId: undefined, | 52 | readyBuildId: undefined, |
| 52 | buildingBuildId: args.buildId, | 53 | buildingBuildId: args.buildId, |
| 54 | typesStorageId: undefined, | ||
| 55 | warnings: [], | ||
| 53 | createdAt: now, | 56 | createdAt: now, |
| 54 | updatedAt: now, | 57 | updatedAt: now, |
| 55 | }); | 58 | }); |
| @@ -144,6 +147,7 @@ export const finishBuild = internalMutation({ · 88 unmodified lines | |||
| 144 | args: { | 147 | args: { |
| 145 | workspaceId: v.id("workspaces"), | 148 | workspaceId: v.id("workspaces"), |
| 146 | buildId: v.string(), | 149 | buildId: v.string(), |
| 150 | signature: v.string(), | ||
| 147 | }, | 151 | }, |
| 148 | handler: async (ctx, args) => { | 152 | handler: async (ctx, args) => { |
| 149 | const now = Date.now(); | 153 | const now = Date.now(); |
| @@ -155,9 +159,11 @@ export const finishBuild = internalMutation({ · 5 unmodified lines | |||
| 155 | if (!state) { | 159 | if (!state) { |
| 156 | await ctx.db.insert("workspaceToolRegistryState", { | 160 | await ctx.db.insert("workspaceToolRegistryState", { |
| 157 | workspaceId: args.workspaceId, | 161 | workspaceId: args.workspaceId, |
| 158 | signature: "", | 162 | signature: args.signature, |
| 159 | readyBuildId: args.buildId, | 163 | readyBuildId: args.buildId, |
| 160 | buildingBuildId: undefined, | 164 | buildingBuildId: undefined, |
| 165 | typesStorageId: undefined, | ||
| 166 | warnings: [], | ||
| 161 | createdAt: now, | 167 | createdAt: now, |
| 162 | updatedAt: now, | 168 | updatedAt: now, |
| 163 | }); | 169 | }); |
| @@ -171,12 +177,64 @@ export const finishBuild = internalMutation({ · 7 unmodified lines | |||
| 171 | 177 | ||
| 172 | await ctx.db.patch(state._id, { | 178 | await ctx.db.patch(state._id, { |
| 173 | readyBuildId: args.buildId, | 179 | readyBuildId: args.buildId, |
| 180 | signature: args.signature, | ||
| 174 | buildingBuildId: undefined, | 181 | buildingBuildId: undefined, |
| 175 | updatedAt: now, | 182 | updatedAt: now, |
| 176 | }); | 183 | }); |
| 177 | }, | 184 | }, |
| 178 | }); | 185 | }); |
| 179 | 186 | ||
| 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 | |||
| 180 | const PRUNE_DELETE_PAGE_SIZE = 10; | 238 | const PRUNE_DELETE_PAGE_SIZE = 10; |
| 181 | const PRUNE_NAMESPACE_SCAN_PAGE_SIZE = 250; | 239 | const PRUNE_NAMESPACE_SCAN_PAGE_SIZE = 250; |
| 182 | 240 | ||
| @@ -363,6 +421,7 @@ export const getToolByPath = internalQuery({ · 180 unmodified lines | |||
| 363 | displayOutput: entry.displayOutput, | 421 | displayOutput: entry.displayOutput, |
| 364 | requiredInputKeys: entry.requiredInputKeys, | 422 | requiredInputKeys: entry.requiredInputKeys, |
| 365 | previewInputKeys: entry.previewInputKeys, | 423 | previewInputKeys: entry.previewInputKeys, |
| 424 | typedRef: entry.typedRef, | ||
| 366 | serializedToolJson: entry.serializedToolJson, | 425 | serializedToolJson: entry.serializedToolJson, |
| 367 | }; | 426 | }; |
| 368 | }, | 427 | }, |
| @@ -377,7 +436,7 @@ export const listToolsByNamespace = internalQuery({ · 8 unmodified lines | |||
| 377 | }, | 436 | }, |
| 378 | handler: async (ctx, args) => { | 437 | handler: async (ctx, args) => { |
| 379 | const namespace = args.namespace.trim().toLowerCase(); | 438 | 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))); |
| 381 | if (!namespace) return []; | 440 | if (!namespace) return []; |
| 382 | 441 | ||
| 383 | const entries = await ctx.db | 442 | const entries = await ctx.db |
| @@ -398,6 +457,7 @@ export const listToolsByNamespace = internalQuery({ · 14 unmodified lines | |||
| 398 | displayOutput: entry.displayOutput, | 457 | displayOutput: entry.displayOutput, |
| 399 | requiredInputKeys: entry.requiredInputKeys, | 458 | requiredInputKeys: entry.requiredInputKeys, |
| 400 | previewInputKeys: entry.previewInputKeys, | 459 | previewInputKeys: entry.previewInputKeys, |
| 460 | typedRef: entry.typedRef, | ||
| 401 | })); | 461 | })); |
| 402 | }, | 462 | }, |
| 403 | }); | 463 | }); |
| @@ -1,7 +1,6 @@ | |||
| 1 | import { v } from "convex/values"; | 1 | import { v } from "convex/values"; |
| 2 | import { internal } from "./_generated/api"; | 2 | import { internal } from "./_generated/api"; |
| 3 | import { mutation } from "./_generated/server"; | 3 | import { customMutation, workspaceMutation, workspaceQuery } from "../../core/src/function-builders"; |
| 4 | import { workspaceMutation, workspaceQuery } from "../../core/src/function-builders"; | ||
| 5 | import { getOrganizationMembership, isAdminRole } from "../../core/src/identity"; | 4 | import { getOrganizationMembership, isAdminRole } from "../../core/src/identity"; |
| 6 | import { | 5 | import { |
| 7 | credentialProviderValidator, | 6 | credentialProviderValidator, |
| @@ -15,7 +14,8 @@ import { · 7 unmodified lines | |||
| 15 | toolSourceTypeValidator, | 14 | toolSourceTypeValidator, |
| 16 | } from "../src/database/validators"; | 15 | } from "../src/database/validators"; |
| 17 | 16 | ||
| 18 | export const bootstrapAnonymousSession = mutation({ | 17 | export const bootstrapAnonymousSession = customMutation({ |
| 18 | method: "POST", | ||
| 19 | args: { | 19 | args: { |
| 20 | sessionId: v.optional(v.string()), | 20 | sessionId: v.optional(v.string()), |
| 21 | accountId: v.optional(v.string()), | 21 | accountId: v.optional(v.string()), |
| @@ -27,6 +27,7 @@ export const bootstrapAnonymousSession = mutation({ · 5 unmodified lines | |||
| 27 | }); | 27 | }); |
| 28 | 28 | ||
| 29 | export const listTasks = workspaceQuery({ | 29 | export const listTasks = workspaceQuery({ |
| 30 | method: "GET", | ||
| 30 | args: {}, | 31 | args: {}, |
| 31 | handler: async (ctx) => { | 32 | handler: async (ctx) => { |
| 32 | return await ctx.runQuery(internal.database.listTasks, { | 33 | return await ctx.runQuery(internal.database.listTasks, { |
| @@ -36,6 +37,7 @@ export const listTasks = workspaceQuery({ · 3 unmodified lines | |||
| 36 | }); | 37 | }); |
| 37 | 38 | ||
| 38 | export const listPendingApprovals = workspaceQuery({ | 39 | export const listPendingApprovals = workspaceQuery({ |
| 40 | method: "GET", | ||
| 39 | args: {}, | 41 | args: {}, |
| 40 | handler: async (ctx) => { | 42 | handler: async (ctx) => { |
| 41 | return await ctx.runQuery(internal.database.listPendingApprovals, { | 43 | return await ctx.runQuery(internal.database.listPendingApprovals, { |
| @@ -45,6 +47,7 @@ export const listPendingApprovals = workspaceQuery({ · 3 unmodified lines | |||
| 45 | }); | 47 | }); |
| 46 | 48 | ||
| 47 | export const upsertAccessPolicy = workspaceMutation({ | 49 | export const upsertAccessPolicy = workspaceMutation({ |
| 50 | method: "POST", | ||
| 48 | requireAdmin: true, | 51 | requireAdmin: true, |
| 49 | args: { | 52 | args: { |
| 50 | id: v.optional(v.string()), | 53 | id: v.optional(v.string()), |
| @@ -73,6 +76,7 @@ export const upsertAccessPolicy = workspaceMutation({ · 22 unmodified lines | |||
| 73 | }); | 76 | }); |
| 74 | 77 | ||
| 75 | export const listAccessPolicies = workspaceQuery({ | 78 | export const listAccessPolicies = workspaceQuery({ |
| 79 | method: "GET", | ||
| 76 | args: {}, | 80 | args: {}, |
| 77 | handler: async (ctx) => { | 81 | handler: async (ctx) => { |
| 78 | return await ctx.runQuery(internal.database.listAccessPolicies, { | 82 | return await ctx.runQuery(internal.database.listAccessPolicies, { |
| @@ -83,6 +87,7 @@ export const listAccessPolicies = workspaceQuery({ · 4 unmodified lines | |||
| 83 | }); | 87 | }); |
| 84 | 88 | ||
| 85 | export const upsertCredential = workspaceMutation({ | 89 | export const upsertCredential = workspaceMutation({ |
| 90 | method: "POST", | ||
| 86 | requireAdmin: true, | 91 | requireAdmin: true, |
| 87 | args: { | 92 | args: { |
| 88 | id: v.optional(v.string()), | 93 | id: v.optional(v.string()), |
| @@ -109,6 +114,7 @@ export const upsertCredential = workspaceMutation({ · 20 unmodified lines | |||
| 109 | }); | 114 | }); |
| 110 | 115 | ||
| 111 | export const listCredentials = workspaceQuery({ | 116 | export const listCredentials = workspaceQuery({ |
| 117 | method: "GET", | ||
| 112 | requireAdmin: true, | 118 | requireAdmin: true, |
| 113 | args: {}, | 119 | args: {}, |
| 114 | handler: async (ctx) => { | 120 | handler: async (ctx) => { |
| @@ -128,6 +134,7 @@ export const listCredentials = workspaceQuery({ · 13 unmodified lines | |||
| 128 | }); | 134 | }); |
| 129 | 135 | ||
| 130 | export const resolveCredential = workspaceQuery({ | 136 | export const resolveCredential = workspaceQuery({ |
| 137 | method: "GET", | ||
| 131 | requireAdmin: true, | 138 | requireAdmin: true, |
| 132 | args: { | 139 | args: { |
| 133 | sourceKey: v.string(), | 140 | sourceKey: v.string(), |
| @@ -143,6 +150,7 @@ export const resolveCredential = workspaceQuery({ · 9 unmodified lines | |||
| 143 | }); | 150 | }); |
| 144 | 151 | ||
| 145 | export const upsertToolSource = workspaceMutation({ | 152 | export const upsertToolSource = workspaceMutation({ |
| 153 | method: "POST", | ||
| 146 | requireAdmin: true, | 154 | requireAdmin: true, |
| 147 | args: { | 155 | args: { |
| 148 | id: v.optional(v.string()), | 156 | id: v.optional(v.string()), |
| @@ -168,6 +176,7 @@ export const upsertToolSource = workspaceMutation({ · 19 unmodified lines | |||
| 168 | }); | 176 | }); |
| 169 | 177 | ||
| 170 | export const listToolSources = workspaceQuery({ | 178 | export const listToolSources = workspaceQuery({ |
| 179 | method: "GET", | ||
| 171 | args: {}, | 180 | args: {}, |
| 172 | handler: async (ctx) => { | 181 | handler: async (ctx) => { |
| 173 | return await ctx.runQuery(internal.database.listToolSources, { | 182 | return await ctx.runQuery(internal.database.listToolSources, { |
| @@ -177,6 +186,7 @@ export const listToolSources = workspaceQuery({ · 3 unmodified lines | |||
| 177 | }); | 186 | }); |
| 178 | 187 | ||
| 179 | export const deleteToolSource = workspaceMutation({ | 188 | export const deleteToolSource = workspaceMutation({ |
| 189 | method: "POST", | ||
| 180 | requireAdmin: true, | 190 | requireAdmin: true, |
| 181 | args: { | 191 | args: { |
| 182 | sourceId: v.string(), | 192 | sourceId: v.string(), |
| @@ -7,6 +7,7 @@ import { · 6 unmodified lines | |||
| 7 | } from "../src/workspaces/handlers"; | 7 | } from "../src/workspaces/handlers"; |
| 8 | 8 | ||
| 9 | export const create = authedMutation({ | 9 | export const create = authedMutation({ |
| 10 | method: "POST", | ||
| 10 | args: { | 11 | args: { |
| 11 | name: v.string(), | 12 | name: v.string(), |
| 12 | organizationId: v.optional(v.id("organizations")), | 13 | organizationId: v.optional(v.id("organizations")), |
| @@ -18,6 +19,7 @@ export const create = authedMutation({ · 5 unmodified lines | |||
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | export const list = optionalAccountQuery({ | 21 | export const list = optionalAccountQuery({ |
| 22 | method: "GET", | ||
| 21 | args: { | 23 | args: { |
| 22 | organizationId: v.optional(v.id("organizations")), | 24 | organizationId: v.optional(v.id("organizations")), |
| 23 | }, | 25 | }, |
| @@ -27,6 +29,7 @@ export const list = optionalAccountQuery({ · 3 unmodified lines | |||
| 27 | }); | 29 | }); |
| 28 | 30 | ||
| 29 | export const generateWorkspaceIconUploadUrl = authedMutation({ | 31 | export const generateWorkspaceIconUploadUrl = authedMutation({ |
| 32 | method: "POST", | ||
| 30 | args: {}, | 33 | args: {}, |
| 31 | handler: async (ctx) => { | 34 | handler: async (ctx) => { |
| 32 | return await generateWorkspaceIconUploadUrlHandler(ctx); | 35 | return await generateWorkspaceIconUploadUrlHandler(ctx); |