convex
Version:
Client for the Convex Cloud
266 lines (251 loc) • 8.18 kB
text/typescript
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)}`);
}
};
}
}