convex
Version:
Client for the Convex Cloud
420 lines (397 loc) • 13.9 kB
text/typescript
import {
ConvexError,
convexToJson,
jsonToConvex,
v,
Validator,
} from "../../values/index.js";
import { GenericDataModel } from "../data_model.js";
import {
ActionBuilder,
DefaultFunctionArgs,
GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
MutationBuilder,
PublicHttpAction,
QueryBuilder,
RegisteredAction,
RegisteredMutation,
RegisteredQuery,
} from "../registration.js";
import { setupActionCalls } from "./actions_impl.js";
import { setupActionVectorSearch } from "./vector_search_impl.js";
import { setupAuth } from "./authentication_impl.js";
import { setupReader, setupWriter } from "./database_impl.js";
import { QueryImpl, QueryInitializerImpl } from "./query_impl.js";
import {
setupActionScheduler,
setupMutationScheduler,
} from "./scheduler_impl.js";
import {
setupStorageActionWriter,
setupStorageReader,
setupStorageWriter,
} from "./storage_impl.js";
async function invokeMutation<
F extends (ctx: GenericMutationCtx<GenericDataModel>, ...args: any) => any,
>(func: F, argsStr: string) {
// TODO(presley): Change the function signature and propagate the requestId from Rust.
// Ok, to mock it out for now, since queries are only running in V8.
const requestId = "";
const args = jsonToConvex(JSON.parse(argsStr));
const mutationCtx = {
db: setupWriter(),
auth: setupAuth(requestId),
storage: setupStorageWriter(requestId),
scheduler: setupMutationScheduler(),
};
const result = await invokeFunction(func, mutationCtx, args as any);
validateReturnValue(result);
return JSON.stringify(convexToJson(result === undefined ? null : result));
}
function validateReturnValue(v: any) {
if (v instanceof QueryInitializerImpl || v instanceof QueryImpl) {
throw new Error(
"Return value is a Query. Results must be retrieved with `.collect()`, `.take(n), `.unique()`, or `.first()`.",
);
}
}
async function invokeFunction<
Ctx,
Args extends any[],
F extends (ctx: Ctx, ...args: Args) => any,
>(func: F, ctx: Ctx, args: Args) {
let result;
try {
result = await Promise.resolve(func(ctx, ...args));
} catch (thrown: unknown) {
throw serializeConvexErrorData(thrown);
}
return result;
}
// Keep in sync with node executor
function serializeConvexErrorData(thrown: unknown) {
if (
typeof thrown === "object" &&
thrown !== null &&
Symbol.for("ConvexError") in thrown
) {
const error = thrown as ConvexError<any>;
error.data = JSON.stringify(
convexToJson(error.data === undefined ? null : error.data),
);
(error as any).ConvexErrorSymbol = Symbol.for("ConvexError");
return error;
} else {
return thrown;
}
}
/**
* Guard against Convex functions accidentally getting included in a browser bundle.
* Convex functions may include secret logic or credentials that should not be
* send to untrusted clients (browsers).
*/
function assertNotBrowser() {
if (
typeof window === "undefined" ||
!(window as any).__convexAllowFunctionsInBrowser
) {
return;
}
// JSDom doesn't count, developers are allowed to use JSDom in Convex functions.
const isRealBrowser =
Object.getOwnPropertyDescriptor(globalThis, "window")
?.get?.toString()
.includes("[native code]") ?? false;
if (isRealBrowser) {
throw new Error("Convex functions should not be imported in the browser.");
}
}
type FunctionDefinition =
| ((ctx: any, args: DefaultFunctionArgs) => any)
| {
args?: Record<string, Validator<any, boolean>>;
handler: (ctx: any, args: DefaultFunctionArgs) => any;
};
function exportArgs(functionDefinition: FunctionDefinition) {
return () => {
let args = v.any();
if (
typeof functionDefinition === "object" &&
functionDefinition.args !== undefined
) {
args = v.object(functionDefinition.args);
}
return JSON.stringify(args.json);
};
}
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* If you're using code generation, use the `mutation` function in
* `convex/_generated/server.d.ts` which is typed for your data model.
*
* @param func - The mutation function. It receives a {@link GenericMutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*
* @public
*/
export const mutationGeneric: MutationBuilder<any, "public"> = (
functionDefinition: FunctionDefinition,
) => {
const func = (
typeof functionDefinition === "function"
? functionDefinition
: functionDefinition.handler
) as RegisteredMutation<"public", any, any>;
// Helpful runtime check that functions are only be registered once
if (func.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
func.isRegistered = true;
func.isMutation = true;
func.isPublic = true;
func.invokeMutation = (argsStr) => invokeMutation(func, argsStr);
func.exportArgs = exportArgs(functionDefinition);
return func;
};
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* If you're using code generation, use the `internalMutation` function in
* `convex/_generated/server.d.ts` which is typed for your data model.
*
* @param func - The mutation function. It receives a {@link GenericMutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*
* @public
*/
export const internalMutationGeneric: MutationBuilder<any, "internal"> = (
functionDefinition: FunctionDefinition,
) => {
const func = (
typeof functionDefinition === "function"
? functionDefinition
: functionDefinition.handler
) as RegisteredMutation<"internal", any, any>;
// Helpful runtime check that functions are only be registered once
if (func.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
func.isRegistered = true;
func.isMutation = true;
func.isInternal = true;
func.invokeMutation = (argsStr) => invokeMutation(func, argsStr);
func.exportArgs = exportArgs(functionDefinition);
return func;
};
async function invokeQuery<
F extends (ctx: GenericQueryCtx<GenericDataModel>, ...args: any) => any,
>(func: F, argsStr: string) {
// TODO(presley): Change the function signature and propagate the requestId from Rust.
// Ok, to mock it out for now, since queries are only running in V8.
const requestId = "";
const args = jsonToConvex(JSON.parse(argsStr));
const queryCtx = {
db: setupReader(),
auth: setupAuth(requestId),
storage: setupStorageReader(requestId),
};
const result = await invokeFunction(func, queryCtx, args as any);
validateReturnValue(result);
return JSON.stringify(convexToJson(result === undefined ? null : result));
}
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* If you're using code generation, use the `query` function in
* `convex/_generated/server.d.ts` which is typed for your data model.
*
* @param func - The query function. It receives a {@link GenericQueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*
* @public
*/
export const queryGeneric: QueryBuilder<any, "public"> = (
functionDefinition: FunctionDefinition,
) => {
const func = (
typeof functionDefinition === "function"
? functionDefinition
: functionDefinition.handler
) as RegisteredQuery<"public", any, any>;
// Helpful runtime check that functions are only be registered once
if (func.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
func.isRegistered = true;
func.isQuery = true;
func.isPublic = true;
func.invokeQuery = (argsStr) => invokeQuery(func, argsStr);
func.exportArgs = exportArgs(functionDefinition);
return func;
};
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* If you're using code generation, use the `internalQuery` function in
* `convex/_generated/server.d.ts` which is typed for your data model.
*
* @param func - The query function. It receives a {@link GenericQueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*
* @public
*/
export const internalQueryGeneric: QueryBuilder<any, "internal"> = (
functionDefinition: FunctionDefinition,
) => {
const func = (
typeof functionDefinition === "function"
? functionDefinition
: functionDefinition.handler
) as RegisteredQuery<"internal", any, any>;
// Helpful runtime check that functions are only be registered once
if (func.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
func.isRegistered = true;
func.isQuery = true;
func.isInternal = true;
func.invokeQuery = (argsStr) => invokeQuery(func as any, argsStr);
func.exportArgs = exportArgs(functionDefinition);
return func;
};
async function invokeAction<
F extends (ctx: GenericActionCtx<GenericDataModel>, ...args: any) => any,
>(func: F, requestId: string, argsStr: string) {
const args = jsonToConvex(JSON.parse(argsStr));
const calls = setupActionCalls(requestId);
const ctx = {
...calls,
auth: setupAuth(requestId),
scheduler: setupActionScheduler(requestId),
storage: setupStorageActionWriter(requestId),
vectorSearch: setupActionVectorSearch(requestId) as any,
};
const result = await invokeFunction(func, ctx, args as any);
return JSON.stringify(convexToJson(result === undefined ? null : result));
}
/**
* Define an action in this Convex app's public API.
*
* If you're using code generation, use the `action` function in
* `convex/_generated/server.d.ts` which is typed for your data model.
*
* @param func - The function. It receives a {@link GenericActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*
* @public
*/
export const actionGeneric: ActionBuilder<any, "public"> = (
functionDefinition: FunctionDefinition,
) => {
const func = (
typeof functionDefinition === "function"
? functionDefinition
: functionDefinition.handler
) as RegisteredAction<"public", any, any>;
// Helpful runtime check that functions are only be registered once
if (func.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
func.isRegistered = true;
func.isAction = true;
func.isPublic = true;
func.invokeAction = (requestId, argsStr) =>
invokeAction(func, requestId, argsStr);
func.exportArgs = exportArgs(functionDefinition);
return func;
};
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* If you're using code generation, use the `internalAction` function in
* `convex/_generated/server.d.ts` which is typed for your data model.
*
* @param func - The function. It receives a {@link GenericActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*
* @public
*/
export const internalActionGeneric: ActionBuilder<any, "internal"> = (
functionDefinition: FunctionDefinition,
) => {
const func = (
typeof functionDefinition === "function"
? functionDefinition
: functionDefinition.handler
) as RegisteredAction<"internal", any, any>;
// Helpful runtime check that functions are only be registered once
if (func.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
func.isRegistered = true;
func.isAction = true;
func.isInternal = true;
func.invokeAction = (requestId, argsStr) =>
invokeAction(func, requestId, argsStr);
func.exportArgs = exportArgs(functionDefinition);
return func;
};
async function invokeHttpAction<
F extends (ctx: GenericActionCtx<GenericDataModel>, request: Request) => any,
>(func: F, request: Request) {
// TODO(presley): Change the function signature and propagate the requestId from Rust.
// Ok, to mock it out for now, since http endpoints are only running in V8.
const requestId = "";
const calls = setupActionCalls(requestId);
const ctx = {
...calls,
auth: setupAuth(requestId),
storage: setupStorageActionWriter(requestId),
scheduler: setupActionScheduler(requestId),
vectorSearch: setupActionVectorSearch(requestId) as any,
};
return await invokeFunction(func, ctx, [request]);
}
/**
* Define a Convex HTTP action.
*
* @param func - The function. It receives an {@link GenericActionCtx} as its first argument, and a `Request` object
* as its second.
* @returns The wrapped function. Route a URL path to this function in `convex/http.js`.
*
* @public
*/
export const httpActionGeneric = (
func: (
ctx: GenericActionCtx<GenericDataModel>,
request: Request,
) => Promise<Response>,
): PublicHttpAction => {
const q = func as unknown as PublicHttpAction;
// Helpful runtime check that functions are only be registered once
if (q.isRegistered) {
throw new Error("Function registered twice " + func);
}
assertNotBrowser();
q.isRegistered = true;
q.isHttp = true;
q.invokeHttpAction = (request) => invokeHttpAction(func as any, request);
return q;
};