@prismicio/custom-types-client
Version:
JavaScript client to interact with the Prismic Custom Types API
616 lines (559 loc) • 18.9 kB
text/typescript
import type * as prismic from "@prismicio/client";
import type { AbortSignalLike, FetchLike, RequestInitLike } from "./types";
import {
BulkUpdateHasExistingDocumentsError,
ConflictError,
ForbiddenError,
InvalidAPIResponse,
InvalidPayloadError,
MissingFetchError,
NotFoundError,
UnauthorizedError,
} from "./errors";
import { BulkUpdateOperation, BulkUpdateTransaction } from "./bulkUpdate";
/**
* The default endpoint for the Prismic Custom Types API.
*/
const DEFAULT_CUSTOM_TYPES_API_ENDPOINT = "https://customtypes.prismic.io";
/**
* Configuration for creating a `CustomTypesClient`.
*/
export type CustomTypesClientConfig = {
/**
* Name of the Prismic repository.
*/
repositoryName: string;
/**
* The Prismic Custom Types API endpoint for the repository. The standard
* Custom Types API endpoint will be used if no value is provided.
*/
endpoint?: string;
/**
* The secure token for accessing the Prismic Custom Types API. This is
* required to call any Custom Type API methods.
*/
token: string;
/**
* The function used to make network requests to the Prismic Custom Types API.
* In environments where a global `fetch` function does not exist, such as
* Node.js, this function must be provided.
*/
fetch?: FetchLike;
/**
* Options provided to the client's `fetch()` on all network requests. These
* options will be merged with internally required options. They can also be
* overridden on a per-query basis using the query's `fetchOptions`
* parameter.
*/
fetchOptions?: RequestInitLike;
};
/**
* Parameters for `CustomTypesClient` methods. Values provided here will
* override the client's default values, if present.
*/
export type CustomTypesClientMethodParams = Partial<
Pick<CustomTypesClientConfig, "repositoryName" | "endpoint" | "token">
>;
/**
* Parameters for client methods that use `fetch()`.
*/
type FetchParams = {
/**
* Options provided to the client's `fetch()` on all network requests. These
* options will be merged with internally required options. They can also be
* overriden on a per-query basis using the query's `fetchOptions` parameter.
*/
fetchOptions?: RequestInitLike;
/**
* An `AbortSignal` provided by an `AbortController`. This allows the network
* request to be cancelled if necessary.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal}
*/
signal?: AbortSignalLike;
};
/**
* Create a `RequestInit` object for a POST `fetch` request. The provided body
* will be run through `JSON.stringify`.
*
* @param body - The request's body.
*
* @returns The `RequestInit` object with the given body.
*/
const createPostFetchRequestInit = <T>(body: T): RequestInitLike => {
return {
method: "POST",
body: JSON.stringify(body),
};
};
/**
* Create a client for the Prismic Custom Types API.
*/
export const createClient = (
...args: ConstructorParameters<typeof CustomTypesClient>
): CustomTypesClient => new CustomTypesClient(...args);
/**
* A client for the Prismic Custom Types API.
*
* @see Custom Types API documentation: {@link https://prismic.io/docs/technologies/custom-types-api}
*/
export class CustomTypesClient {
/**
* Name of the Prismic repository.
*/
repositoryName: string;
/**
* The Prismic Custom Types API endpoint for the repository. The standard
* Custom Types API endpoint will be used if no value is provided.
*
* @defaultValue `https://customtypes.prismic.io`
*/
endpoint: string;
/**
* The secure token for accessing the Prismic Custom Types API. This is
* required to call any Custom Type API methods.
*/
token: string;
/**
* The function used to make network requests to the Prismic Custom Types API.
* In environments where a global `fetch` function does not exist, such as
* Node.js, this function must be provided.
*/
fetchFn: FetchLike;
/**
* Options provided to the client's `fetch()` on all network requests. These
* options will be merged with internally required options. They can also be
* overriden on a per-query basis using the query's `fetchOptions` parameter.
*/
fetchOptions?: RequestInitLike;
/**
* Create a client for the Prismic Custom Types API.
*/
constructor(config: CustomTypesClientConfig) {
this.repositoryName = config.repositoryName;
this.endpoint = config.endpoint || DEFAULT_CUSTOM_TYPES_API_ENDPOINT;
this.token = config.token;
this.fetchOptions = config.fetchOptions;
// TODO: Remove the following `if` statement in v2.
//
// v1 erroneously assumed `/customtypes` would be part of
// `this.endpoint`, forcing all custom endpoints to include
// `/customtypes`.
//
// The client no longer assumes `/customtypes`. This `if`
// statement ensures backwards compatibility with existing
// custom endpoints that includes `/customtypes`.
if (/\/customtypes\/?$/.test(this.endpoint)) {
this.endpoint = this.endpoint.replace(/\/customtypes\/?$/, "");
}
if (typeof config.fetch === "function") {
this.fetchFn = config.fetch;
} else if (typeof globalThis.fetch === "function") {
this.fetchFn = globalThis.fetch;
} else {
throw new MissingFetchError(
"A valid fetch implementation was not provided. In environments where fetch is not available (including Node.js), a fetch implementation must be provided via a polyfill or the `fetch` config parameter.",
);
}
// If the global fetch function is used, we must bind it to the global scope.
if (this.fetchFn === globalThis.fetch) {
this.fetchFn = this.fetchFn.bind(globalThis);
}
}
/**
* Returns all Custom Types models from the Prismic repository.
*
* @typeParam TCustomType - The Custom Type returned from the API.
*
* @param params - Parameters to override the client's default configuration.
*
* @returns All Custom Type models from the Prismic repository.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
*/
async getAllCustomTypes<TCustomType extends prismic.CustomTypeModel>(
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TCustomType[]> {
return await this.fetch<TCustomType[]>("./customtypes", params);
}
/**
* Returns a Custom Type model with a given ID from the Prismic repository.
*
* @typeParam TCustomType - The Custom Type returned from the API.
*
* @param id - ID of the Custom Type.
* @param params - Parameters to override the client's default configuration.
*
* @returns The Custom Type model from the Prismic repository.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link NotFoundError} Thrown if a Custom Type with the given ID
* cannot be found.
*/
async getCustomTypeByID<TCustomType extends prismic.CustomTypeModel>(
id: string,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TCustomType> {
return await this.fetch<TCustomType>(`./customtypes/${id}`, params);
}
/**
* Inserts a Custom Type model to the Prismic repository.
*
* @typeParam TCustomType - The Custom Type to insert.
*
* @param customType - The Custom Type to insert.
* @param params - Parameters to override the client's default configuration.
*
* @returns The inserted Custom Type.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link InvalidPayloadError} Thrown if an invalid Custom Type is
* provided.
* @throws {@link ConflictError} Thrown if a Custom Type with the given ID
* already exists.
*/
async insertCustomType<TCustomType extends prismic.CustomTypeModel>(
customType: TCustomType,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TCustomType> {
await this.fetch<undefined>(
"./customtypes/insert",
params,
createPostFetchRequestInit(customType),
);
return customType;
}
/**
* Updates a Custom Type model from the Prismic repository.
*
* @typeParam TCustomType - The updated Custom Type.
*
* @param customType - The updated Custom Type.
* @param params - Parameters to override the client's default configuration.
*
* @returns The updated Custom Type.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link InvalidPayloadError} Thrown if an invalid Custom Type is
* provided.
* @throws {@link NotFoundError} Thrown if a Custom Type with the given ID
* cannot be found.
*/
async updateCustomType<TCustomType extends prismic.CustomTypeModel>(
customType: TCustomType,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TCustomType> {
await this.fetch<undefined>(
"./customtypes/update",
params,
createPostFetchRequestInit(customType),
);
return customType;
}
/**
* Removes a Custom Type model from the Prismic repository.
*
* @typeParam TCustomTypeID - The ID of the Custom Type.
*
* @param id - The ID of the Custom Type to remove.
* @param params - Parameters to override the client's default configuration.
*
* @returns The ID of the removed Custom Type.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
*/
async removeCustomType<TCustomTypeID extends string>(
id: TCustomTypeID,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TCustomTypeID> {
await this.fetch<undefined>(`./customtypes/${id}`, params, {
method: "DELETE",
});
return id;
}
/**
* Returns all Shared Slice models from the Prismic repository.
*
* @typeParam TSharedSliceModel - The Shared Slice model returned from the
* API.
*
* @param params - Parameters to override the client's default configuration.
*
* @returns All Shared Slice models from the Prismic repository.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
*/
async getAllSharedSlices<TSharedSliceModel extends prismic.SharedSliceModel>(
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TSharedSliceModel[]> {
return await this.fetch<TSharedSliceModel[]>("./slices", params);
}
/**
* Returns a Shared Slice model with a given ID from the Prismic repository.
*
* @typeParam TSharedSliceModel - The Shared Slice model returned from the
* API.
*
* @param id - ID of the Shared Slice.
* @param params - Parameters to override the client's default configuration.
*
* @returns The Shared Slice model from the Prismic repository.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link NotFoundError} Thrown if a Shared Slice with the given ID
* cannot be found.
*/
async getSharedSliceByID<TSharedSliceModel extends prismic.SharedSliceModel>(
id: string,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TSharedSliceModel> {
return await this.fetch<TSharedSliceModel>(`./slices/${id}`, params);
}
/**
* Inserts a Shared Slice model to the Prismic repository.
*
* @typeParam TSharedSliceModel - The Shared Slice model to insert.
*
* @param slice - The Shared Slice model to insert.
* @param params - Parameters to override the client's default configuration.
*
* @returns The inserted Shared Slice model.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link InvalidPayloadError} Thrown if an invalid Shared Slice model
* is provided.
* @throws {@link ConflictError} Thrown if a Shared Slice with the given ID
* already exists.
*/
async insertSharedSlice<TSharedSliceModel extends prismic.SharedSliceModel>(
slice: TSharedSliceModel,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TSharedSliceModel> {
await this.fetch(
"./slices/insert",
params,
createPostFetchRequestInit(slice),
);
return slice;
}
/**
* Updates a Shared Slice model from the Prismic repository.
*
* @typeParam TSharedSliceModel - The updated Shared Slice model.
*
* @param slice - The updated Shared Slice model.
* @param params - Parameters to override the client's default configuration.
*
* @returns The updated Shared Slice model.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link InvalidPayloadError} Thrown if an invalid Shared Slice model
* is provided.
* @throws {@link NotFoundError} Thrown if a Shared Slice with the given ID
* cannot be found.
*/
async updateSharedSlice<TSharedSliceModel extends prismic.SharedSliceModel>(
slice: TSharedSliceModel,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TSharedSliceModel> {
await this.fetch(
"./slices/update",
params,
createPostFetchRequestInit(slice),
);
return slice;
}
/**
* Removes a Shared Slice model from the Prismic repository.
*
* @typeParam TSharedSliceID - The ID of the Shared Slice.
*
* @param id - The ID of the Shared Slice to remove.
* @param params - Parameters to override the client's default configuration.
*
* @returns The ID of the removed Shared Slice.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
*/
async removeSharedSlice<TSharedSliceID extends string>(
id: TSharedSliceID,
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<TSharedSliceID> {
await this.fetch(`./slices/${id}`, params, {
method: "DELETE",
});
return id;
}
/**
* Performs multiple insert, update, and/or delete operations in a single
* transaction.
*
* @example
*
* ```ts
* const bulkUpdateTransaction = createBulkUpdateTransaction();
* bulkUpdateTransaction.insertCustomType(myCustomType);
* bulkUpdateTransaction.deleteSlice(mySlice);
*
* await client.bulkUpdate(bulkUpdateTransaction);
* ```
*
* @param operations - A `BulkUpdateTransaction` containing all operations or
* an array of objects describing an operation.
* @param params - Parameters that determine how the method behaves and for
* overriding the client's default configuration.
*
* @returns An array of objects describing the operations.
*/
async bulkUpdate(
operations: BulkUpdateTransaction | BulkUpdateOperation[],
params?: CustomTypesClientMethodParams & FetchParams,
): Promise<BulkUpdateOperation[]> {
const resolvedOperations =
operations instanceof BulkUpdateTransaction
? operations.operations
: operations;
await this.fetch(
"./bulk-update",
params,
createPostFetchRequestInit({
changes: resolvedOperations,
}),
);
return resolvedOperations;
}
/**
* Performs a network request using the configured `fetch` function. It
* assumes all successful responses will have a JSON content type. It also
* normalizes unsuccessful network requests.
*
* @typeParam T - The JSON response.
*
* @param path - URL to the resource to fetch.
* @param params - Parameters to override the client's default configuration.
* @param requestInit - `RequestInit` overrides for the `fetch` request.
*
* @returns The response from the network request, if any.
*
* @throws {@link ForbiddenError} Thrown if the client is unauthorized to make
* requests.
* @throws {@link InvalidPayloadError} Thrown if the given body is invalid.
* @throws {@link ConflictError} Thrown if an entity with the given ID already
* exists.
* @throws {@link NotFoundError} Thrown if the requested entity could not be
* found.
*/
private async fetch<T = unknown>(
path: string,
params: Partial<CustomTypesClientMethodParams> & FetchParams = {},
requestInit: RequestInitLike = {},
): Promise<T> {
const endpoint = params.endpoint || this.endpoint;
const url = new URL(
path,
endpoint.endsWith("/") ? endpoint : `${endpoint}/`,
).toString();
const res = await this.fetchFn(url, {
...this.fetchOptions,
...requestInit,
...params.fetchOptions,
headers: {
"Content-Type": "application/json",
repository: params.repositoryName || this.repositoryName,
Authorization: `Bearer ${params.token || this.token}`,
...this.fetchOptions?.headers,
...requestInit.headers,
...params.fetchOptions?.headers,
},
signal:
params.fetchOptions?.signal ||
params.signal ||
requestInit.signal ||
this.fetchOptions?.signal,
});
switch (res.status) {
// Successful
// - Successfully get one or more Custom Types
// - Successfully get one or more Shared Slices
case 200: {
return await res.json();
}
// Created
// - Successfully insert a Custom Type
// - Successfully insert a Shared Slice
case 201:
// No Content
// - Successfully update a Custom Type
// - Successfully delete a Custom Type
// - Successfully update a Shared Slice
// - Successfully delete a Shared Slice
case 204: {
// We use `any` since we don't have a concrete value we can return. We
// let the call site define what the return type is with the `T` generic.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return undefined as any;
}
// Bad Request
// - Invalid body sent
case 400: {
const text = await res.text();
throw new InvalidPayloadError(text, { url, response: text });
}
// Unauthorized
// - User does not have access to requested repository
case 401: {
const text = await res.text();
throw new UnauthorizedError(text, { url, response: text });
}
// Forbidden
// - Missing token
// - Incorrect token
// - Has existing documents (cannot process)
case 403: {
const json = await res.json();
if ("hasExistingDocuments" in json && json.hasExistingDocuments) {
throw new BulkUpdateHasExistingDocumentsError(
"A custom type with published documents cannot be deleted. Delete all of the custom type's documents or remove the delete operation from the request before trying again.",
{ url, response: json },
);
}
throw new ForbiddenError(json.message, { url, response: json });
}
// Conflict
// - Insert a Custom Type with same ID as an existing Custom Type
// - Insert a Shared Slice with same ID as an existing Shared Slice
case 409: {
throw new ConflictError(
"The provided ID is already used. A unique ID must be provided.",
{ url },
);
}
// Not Found
// - Get a Custom Type with no matching ID
// - Get a Shared Slice with no matching ID
case 404:
// Unprocessable Entity
// - Update a Custom Type with no matching ID
// - Update a Shared Slice with no matching ID
case 422: {
throw new NotFoundError(
"An entity with a matching ID could not be found.",
{ url },
);
}
}
throw new InvalidAPIResponse("An invalid API response was returned", {
url,
});
}
}