UNPKG

convex

Version:

Client for the Convex Cloud

266 lines (251 loc) 8.18 kB
import { convexToJson, jsonToConvex } from "../values/index.js"; import { STATUS_CODE_UDF_FAILED } from "../common/index.js"; import { version } from "../index.js"; import { ActionNames, GenericAPI, MutationNames, NamedAction, NamedMutation, NamedQuery, QueryNames, } from "../api/index.js"; import { ClientConfiguration } from "./client_config.js"; import { createError, logToConsole } from "./logging.js"; /** Isomorphic `fetch` for Node.js and browser usage. */ const hasFetch = typeof window !== "undefined" && typeof window.fetch !== "undefined"; type WindowFetch = typeof window.fetch; const fetch: WindowFetch = hasFetch ? window.fetch : (...args) => import("node-fetch").then(({ default: fetch }) => (fetch as unknown as WindowFetch)(...args) ); // TODO Typedoc doesn't generate documentation for the comment below perhaps // because it's a callable interface. /** * An interface to execute a Convex query function on the server. * * @public */ export interface Query<F extends (...args: any[]) => Promise<any>> { /** * Execute the query on the server, returning a `Promise` of the return value. * * @param args - Arguments for the query. * @returns The result of the query. */ (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>; } // TODO Typedoc doesn't generate documentation for the comment below perhaps // because it's a callable interface. /** * An interface to execute a Convex mutation function on the server. * * @public */ export interface Mutation<F extends (...args: any[]) => Promise<any>> { /** * Execute the mutation on the server, returning a `Promise` of its return value. * * @param args - Arguments for the mutation. * @returns The return value of the server-side function call. */ (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>; } // TODO Typedoc doesn't generate documentation for the comment below perhaps // because it's a callable interface. /** * An interface to execute a Convex action on the server. * * @internal */ export interface Action<F extends (...args: any[]) => Promise<any>> { /** * Execute the action on the server, returning a `Promise` of its return value. * * @param args - Arguments for the action. * @returns The return value of the server-side action call. */ (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>; } /** * A Convex client that runs queries and mutations over HTTP. * * This is appropriate for server-side code (like Netlify Lambdas) or non-reactive * webapps. * * If you're building a React app, consider using * {@link react.ConvexReactClient} instead. * * * @public */ export class ConvexHttpClient<API extends GenericAPI> { private readonly address: string; private auth?: string; private debug: boolean; constructor(clientConfig: ClientConfiguration) { this.address = `${clientConfig.address}/api`; this.debug = true; } /** * Obtain the {@link ConvexHttpClient}'s URL to its backend. * * @returns The URL to the Convex backend, including the client's API version. */ backendUrl(): string { return this.address; } /** * Set the authentication token to be used for subsequent queries and mutations. * * Should be called whenever the token changes (i.e. due to expiration and refresh). * * @param value - JWT-encoded OpenID Connect identity token. */ setAuth(value: string) { this.auth = value; } /** * Clear the current authentication token if set. */ clearAuth() { this.auth = undefined; } /** * Sets whether the result log lines should be printed on the console or not. * * @internal */ setDebug(debug: boolean) { this.debug = debug; } /** * Construct a new {@link Query}. * * @param name - The name of the query function. * @returns The {@link Query} object with that name. */ query<Name extends QueryNames<API>>(name: Name) { return async ( ...args: Parameters<NamedQuery<API, Name>> ): Promise<ReturnType<NamedQuery<API, Name>>> => { // Interpret the arguments as a Convex array, and then serialize // it to JSON. const argsJSON = JSON.stringify(convexToJson(args)); const argsComponent = encodeURIComponent(argsJSON); const url = `${this.address}/${version}/udf?path=${name}&args=${argsComponent}`; const headers: Record<string, string> = this.auth ? { Authorization: `Bearer ${this.auth}` } : {}; const response = await fetch(url, { credentials: "include", headers: headers, }); if (!response.ok && response.status != STATUS_CODE_UDF_FAILED) { throw new Error(await response.text()); } const respJSON = await response.json(); const value = jsonToConvex(respJSON.value); for (const line of respJSON.logs) { logToConsole("info", "query", name, line); } if (!respJSON.success) { throw createError("query", name, value as string); } return value as Awaited<ReturnType<NamedQuery<API, Name>>>; }; } /** * Construct a new {@link Mutation}. * * @param name - The name of the mutation function. * @returns The {@link Mutation} object with that name. */ mutation<Name extends MutationNames<API>>(name: Name) { return async ( ...args: Parameters<NamedMutation<API, Name>> ): Promise<ReturnType<NamedMutation<API, Name>>> => { // Interpret the arguments as a Convex array and then serialize to JSON. const body = JSON.stringify({ path: name, args: convexToJson(args), tokens: [], }); const headers: Record<string, string> = { "Content-Type": "application/json", }; if (this.auth) { headers["Authorization"] = `Bearer ${this.auth}`; } const response = await fetch(`${this.address}/${version}/udf`, { body, method: "POST", headers: headers, credentials: "include", }); if (!response.ok && response.status != STATUS_CODE_UDF_FAILED) { throw new Error(await response.text()); } const respJSON = await response.json(); const value = jsonToConvex(respJSON.value); for (const line of respJSON.logs) { logToConsole("info", "mutation", name, line); } if (!respJSON.success) { throw createError("mutation", name, value as string); } return value as Awaited<ReturnType<NamedQuery<API, Name>>>; }; } /** * Construct a new {@link Action}. * * @param name - The name of the action. * @returns The {@link Action} object with that name. * @public */ action<Name extends ActionNames<API>>(name: Name) { return async ( ...args: Parameters<NamedAction<API, Name>> ): Promise<ReturnType<NamedAction<API, Name>>> => { // Interpret the arguments as a Convex array and then serialize to JSON. const body = JSON.stringify({ path: name, args: convexToJson(args), debug: this.debug, }); const headers: Record<string, string> = { "Content-Type": "application/json", }; if (this.auth) { headers["Authorization"] = `Bearer ${this.auth}`; } const response = await fetch(`${this.address}/action`, { body, method: "POST", headers: headers, credentials: "include", }); if (!response.ok && response.status != STATUS_CODE_UDF_FAILED) { throw new Error(await response.text()); } const respJSON = await response.json(); for (const line of respJSON.logLines ?? []) { logToConsole("info", "action", name, line); } switch (respJSON.status) { case "success": // Validate that the response is a valid Convex value. jsonToConvex(respJSON.value); return respJSON.value as Awaited<ReturnType<NamedAction<API, Name>>>; case "error": throw new Error(respJSON.errorMessage); default: throw new Error(`Invalid response: ${JSON.stringify(respJSON)}`); } }; } }