@prismicio/client
Version:
The official JavaScript + TypeScript client library for Prismic
1,548 lines (1,432 loc) • 42.7 kB
text/typescript
import { devMsg } from "./lib/devMsg"
import { getPreviewCookie } from "./lib/getPreviewCookie"
import type { ResponseLike } from "./lib/request"
import {
type AbortSignalLike,
type FetchLike,
type RequestInitLike,
request,
} from "./lib/request"
import { throttledWarn } from "./lib/throttledWarn"
import type { Query } from "./types/api/query"
import type { Ref } from "./types/api/ref"
import type { Repository } from "./types/api/repository"
import type { PrismicDocument } from "./types/value/document"
import {
ForbiddenError,
NotFoundError,
ParsingError,
PreviewTokenExpiredError,
PrismicError,
RefExpiredError,
RefNotFoundError,
RepositoryNotFoundError,
} from "./errors"
import { type LinkResolverFunction, asLink } from "./helpers/asLink"
import { type BuildQueryURLArgs, buildQueryURL } from "./buildQueryURL"
import { filter } from "./filter"
import { getRepositoryEndpoint } from "./getRepositoryEndpoint"
import { getRepositoryName } from "./getRepositoryName"
import { isRepositoryEndpoint } from "./isRepositoryEndpoint"
const MAX_PAGE_SIZE = 100
const REPOSITORY_CACHE_TTL = 5000
const GET_ALL_QUERY_DELAY = 500
const MAX_INVALID_REF_RETRY_ATTEMPTS = 3
/**
* Extracts a document type with a matching `type` property from a union of
* document types.
*/
type ExtractDocumentType<
TDocuments extends PrismicDocument,
TDocumentType extends TDocuments["type"],
> =
Extract<TDocuments, { type: TDocumentType }> extends never
? TDocuments
: Extract<TDocuments, { type: TDocumentType }>
/**
* The minimum required properties to treat as an HTTP Request for automatic
* Prismic preview support.
*/
export type HttpRequestLike =
| // Web API Request
{
headers?: {
get(name: string): string | null
}
url?: string
}
// Express-style request
| {
headers?: {
cookie?: string
}
query?: Record<string, unknown>
}
/**
* A function that returns a ref string. Used to configure which ref the client
* queries content from.
*/
type GetRef = (
params?: Pick<BuildQueryURLArgs, "accessToken"> & FetchParams,
) => string | undefined | Promise<string | undefined>
/** Parameters for client methods that use `fetch()`. */
export 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
/** @deprecated Move to `fetchOptions.signal`: */
// TODO: Remove in v8.
signal?: AbortSignalLike
}
/** Prismic client configuration. */
export type ClientConfig = {
/**
* The client's Content API endpoint.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
documentAPIEndpoint?: string
/**
* The secure token used for the Content API.
*
* @see {@link https://prismic.io/docs/fetch-content#content-visibility}
*/
accessToken?: string
/**
* The version of the repository's content. It can optionally be a function
* that returns a ref.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
ref?: string | GetRef
/**
* A list of route resolver objects that define how a document's `url`
* property is resolved.
*
* @see {@link https://prismic.io/docs/routes}
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
routes?: NonNullable<BuildQueryURLArgs["routes"]>
/**
* The URL used for link or content relationship fields that point to an
* archived or deleted page.
*
* @see {@link https://prismic.io/docs/routes}
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
brokenRoute?: NonNullable<BuildQueryURLArgs["brokenRoute"]>
/**
* Default parameters sent with each Content API request. These parameters can
* be overridden on each method.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
defaultParams?: Omit<
BuildQueryURLArgs,
"ref" | "integrationFieldsRef" | "accessToken" | "routes" | "brokenRoute"
>
/**
* The `fetch` function used to make network requests.
*
* @default The global `fetch` function.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
fetch?: FetchLike
/**
* The default `fetch` options sent with each Content API request. These
* parameters can be overriden on each method.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
fetchOptions?: RequestInitLike
}
/**
* Parameters specific to client methods that fetch all documents. These methods
* start with `getAll` (e.g. `getAllByType`).
*/
type GetAllParams = {
/**
* Limit the number of documents queried.
*
* @default No limit.
*/
limit?: number
}
/**
* A client for fetching content from a Prismic repository.
*
* @see {@link https://prismic.io/docs/fetch-content}
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client}
*/
export class Client<TDocuments extends PrismicDocument = PrismicDocument> {
/**
* The client's Content API endpoint.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
documentAPIEndpoint: string
/**
* The secure token used for the Content API.
*
* @see {@link https://prismic.io/docs/fetch-content#content-visibility}
*/
accessToken?: string
/**
* A list of route resolver objects that define how a document's `url`
* property is resolved.
*
* @see {@link https://prismic.io/docs/routes}
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
routes?: NonNullable<BuildQueryURLArgs["routes"]>
/**
* The URL used for link or content relationship fields that point to an
* archived or deleted page.
*
* @see {@link https://prismic.io/docs/routes}
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
brokenRoute?: NonNullable<BuildQueryURLArgs["brokenRoute"]>
/**
* Default parameters sent with each Content API request. These parameters can
* be overridden on each method.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
defaultParams?: Omit<
BuildQueryURLArgs,
"ref" | "integrationFieldsRef" | "accessToken" | "routes"
>
/**
* The `fetch` function used to make network requests.
*
* @default The global `fetch` function.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
fetchFn: FetchLike
/**
* The default `fetch` options sent with each Content API request. These
* parameters can be overriden on each method.
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#config-options}
*/
fetchOptions: RequestInitLike
#repositoryName: string | undefined
#getRef?: GetRef
#autoPreviews = true
#autoPreviewsRequest?: HttpRequestLike
#cachedRepository: Repository | undefined
#cachedRepositoryExpiration = 0 // Timestamp
/**
* @param repositoryNameOrEndpoint - The Prismic repository name or full
* Content API endpoint for the repository.
* @param config - Client configuration.
*/
constructor(repositoryNameOrEndpoint: string, config: ClientConfig = {}) {
const {
documentAPIEndpoint,
accessToken,
ref,
routes,
brokenRoute,
defaultParams,
fetchOptions = {},
fetch = globalThis.fetch?.bind(globalThis),
} = config
if (isRepositoryEndpoint(repositoryNameOrEndpoint)) {
try {
this.repositoryName = getRepositoryName(repositoryNameOrEndpoint)
} catch {
console.warn(
`[/client] A repository name could not be inferred from the provided endpoint (\`${repositoryNameOrEndpoint}\`). Some methods will be disabled. Create the client using a repository name to prevent this warning. For more details, see ${devMsg("prefer-repository-name")}`,
)
}
this.documentAPIEndpoint = documentAPIEndpoint || repositoryNameOrEndpoint
} else {
this.repositoryName = repositoryNameOrEndpoint
this.documentAPIEndpoint =
documentAPIEndpoint || getRepositoryEndpoint(repositoryNameOrEndpoint)
}
if (!fetch) {
throw new PrismicError(
"A valid fetch implementation was not provided. In environments where fetch is not available, a fetch implementation must be provided via a polyfill or the `fetch` option.",
undefined,
undefined,
)
}
if (typeof fetch !== "function") {
throw new PrismicError(
`fetch must be a function, but received: ${typeof fetch}`,
undefined,
undefined,
)
}
if (!isRepositoryEndpoint(this.documentAPIEndpoint)) {
throw new PrismicError(
`documentAPIEndpoint is not a valid URL: ${documentAPIEndpoint}`,
undefined,
undefined,
)
}
if (
isRepositoryEndpoint(repositoryNameOrEndpoint) &&
documentAPIEndpoint &&
repositoryNameOrEndpoint !== documentAPIEndpoint
) {
console.warn(
`[/client] Multiple incompatible endpoints were provided. Create the client using a repository name to prevent this error. For more details, see ${devMsg("prefer-repository-name")}`,
)
}
if (
process.env.NODE_ENV === "development" &&
/\.prismic\.io\/(?!api\/v2\/?)/i.test(this.documentAPIEndpoint)
) {
throw new PrismicError(
"@prismicio/client only supports Prismic Rest API V2. Please provide only the repository name to the first createClient() parameter or use the getRepositoryEndpoint() helper to generate a valid Rest API V2 endpoint URL.",
undefined,
undefined,
)
}
if (
process.env.NODE_ENV === "development" &&
/\.prismic\.io$/i.test(
new URL(this.documentAPIEndpoint).hostname,
) &&
!/\.cdn\.prismic\.io$/i.test(
new URL(this.documentAPIEndpoint).hostname,
)
) {
console.warn(
`[/client] The client was created with a non-CDN endpoint. Convert it to the CDN endpoint for better performance. For more details, see ${devMsg("endpoint-must-use-cdn")}`,
)
}
this.accessToken = accessToken
this.routes = routes
this.brokenRoute = brokenRoute
this.defaultParams = defaultParams
this.fetchOptions = fetchOptions
this.fetchFn = fetch
this.graphQLFetch = this.graphQLFetch.bind(this)
if (ref) {
this.queryContentFromRef(ref)
}
}
/** The Prismic repository's name. */
set repositoryName(value: string) {
this.#repositoryName = value
}
/** The Prismic repository's name. */
get repositoryName(): string {
if (!this.#repositoryName) {
throw new PrismicError(
`A repository name is required for this method but one could not be inferred from the provided API endpoint (\`${this.documentAPIEndpoint}\`). To fix this error, provide a repository name when creating the client. For more details, see ${devMsg("prefer-repository-name")}`,
undefined,
undefined,
)
}
return this.#repositoryName
}
/** @deprecated Replace with `documentAPIEndpoint`. */
// TODO: Remove in v8.
set endpoint(value: string) {
this.documentAPIEndpoint = value
}
/** @deprecated Replace with `documentAPIEndpoint`. */
// TODO: Remove in v8.
get endpoint(): string {
return this.documentAPIEndpoint
}
/**
* Enables the client to automatically query content from a preview session.
*
* @example
*
* ```ts
* client.enableAutoPreviews()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#enableautopreviews}
*/
enableAutoPreviews(): void {
this.#autoPreviews = true
}
/**
* Enables the client to automatically query content from a preview session
* using an HTTP request object.
*
* @example
*
* ```ts
* client.enableAutoPreviewsFromReq(req)
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#enableautopreviewsfromreq}
*/
enableAutoPreviewsFromReq(request: HttpRequestLike): void {
this.enableAutoPreviews()
this.#autoPreviewsRequest = request
}
/**
* Disables the client from automatically querying content from a preview
* session.
*
* @example
*
* ```ts
* client.disableAutoPreviews()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#disableautopreviews}
*/
disableAutoPreviews(): void {
this.#autoPreviews = false
this.#autoPreviewsRequest = undefined
}
/**
* Fetches pages based on the `params` argument. Results are paginated.
*
* @example
*
* ```ts
* const response = await client.get({ pageSize: 10 })
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#get}
*/
async get<TDocument extends TDocuments>(
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<TDocument>> {
const response = await this.#internalGet(params)
return await response.json()
}
/**
* Fetches the first page returned based on the `params` argument.
*
* @example
*
* ```ts
* const page = await client.getFirst()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getfirst}
*/
async getFirst<TDocument extends TDocuments>(
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<TDocument> {
const actualParams =
params?.page || params?.pageSize ? params : { ...params, pageSize: 1 }
const response = await this.#internalGet(actualParams)
const { results }: Query<TDocument> = await response.clone().json()
if (results[0]) {
return results[0]
}
throw new NotFoundError(
"No documents were returned",
response.url,
undefined,
)
}
/**
* Fetches all pages based on the `params` argument. This method may make
* multiple network requests to fetch all matching pages.
*
* @example
*
* ```ts
* const pages = await client.dangerouslyGetAll()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#dangerouslygetall}
*/
async dangerouslyGetAll<TDocument extends TDocuments>(
params: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams = {},
): Promise<TDocument[]> {
const { limit = Infinity, ...actualParams } = params
const resolvedParams = {
...actualParams,
pageSize: Math.min(
limit,
actualParams.pageSize || this.defaultParams?.pageSize || MAX_PAGE_SIZE,
),
}
const documents: TDocument[] = []
let latestResult: Query<TDocument> | undefined
while (
(!latestResult || latestResult.next_page) &&
documents.length < limit
) {
const page = latestResult ? latestResult.page + 1 : undefined
latestResult = await this.get<TDocument>({ ...resolvedParams, page })
documents.push(...latestResult.results)
if (latestResult.next_page) {
await new Promise((res) => setTimeout(res, GET_ALL_QUERY_DELAY))
}
}
return documents.slice(0, limit)
}
/**
* Fetches a page with a specific ID.
*
* @example
*
* ```ts
* const page = await client.getByID("WW4bKScAAMAqmluX")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbyid}
*/
async getByID<TDocument extends TDocuments>(
id: string,
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<TDocument> {
return await this.getFirst<TDocument>(
appendFilters(params, filter.at("document.id", id)),
)
}
/**
* Fetches pages with specific IDs. Results are paginated.
*
* @example
*
* ```ts
* const response = await client.getByIDs([
* "WW4bKScAAMAqmluX",
* "U1kTRgEAAC8A5ldS",
* ])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbyids}
*/
async getByIDs<TDocument extends TDocuments>(
ids: string[],
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<TDocument>> {
return await this.get<TDocument>(
appendFilters(params, filter.in("document.id", ids)),
)
}
/**
* Fetches pages with specific IDs. This method may make multiple network
* requests to fetch all matching pages.
*
* @example
*
* ```ts
* const pages = await client.getAllByIDs([
* "WW4bKScAAMAqmluX",
* "U1kTRgEAAC8A5ldS",
* ])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getallbyids}
*/
async getAllByIDs<TDocument extends TDocuments>(
ids: string[],
params?: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams,
): Promise<TDocument[]> {
return await this.dangerouslyGetAll<TDocument>(
appendFilters(params, filter.in("document.id", ids)),
)
}
/**
* Fetches a page with a specific UID and type.
*
* @example
*
* ```ts
* const page = await client.getByUID("blog_post", "my-first-post")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbyuid}
*/
async getByUID<
TDocument extends TDocuments,
TDocumentType extends TDocument["type"] = TDocument["type"],
>(
documentType: TDocumentType,
uid: string,
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<ExtractDocumentType<TDocument, TDocumentType>> {
return await this.getFirst<ExtractDocumentType<TDocument, TDocumentType>>(
appendFilters(
params,
filter.at("document.type", documentType),
filter.at(`my.${documentType}.uid`, uid),
),
)
}
/**
* Fetches pages with specific UIDs and a specific type. Results are
* paginated.
*
* @example
*
* ```ts
* const response = await client.getByUIDs("blog_post", [
* "my-first-post",
* "my-second-post",
* ])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbyuids}
*/
async getByUIDs<
TDocument extends TDocuments,
TDocumentType extends TDocument["type"] = TDocument["type"],
>(
documentType: TDocumentType,
uids: string[],
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<ExtractDocumentType<TDocument, TDocumentType>>> {
return await this.get<ExtractDocumentType<TDocument, TDocumentType>>(
appendFilters(
params,
filter.at("document.type", documentType),
filter.in(`my.${documentType}.uid`, uids),
),
)
}
/**
* Fetches pages with specific UIDs and a specific type. This method may make
* multiple network requests to fetch all matching pages.
*
* @example
*
* ```ts
* const pages = await client.getAllByUIDs("blog_post", [
* "my-first-post",
* "my-second-post",
* ])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getallbyuids}
*/
async getAllByUIDs<
TDocument extends TDocuments,
TDocumentType extends TDocument["type"] = TDocument["type"],
>(
documentType: TDocumentType,
uids: string[],
params?: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams,
): Promise<ExtractDocumentType<TDocument, TDocumentType>[]> {
return await this.dangerouslyGetAll<
ExtractDocumentType<TDocument, TDocumentType>
>(
appendFilters(
params,
filter.at("document.type", documentType),
filter.in(`my.${documentType}.uid`, uids),
),
)
}
/**
* Fetches a specific single type page.
*
* @example
*
* ```ts
* const page = await client.getSingle("settings")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getsingle}
*/
async getSingle<
TDocument extends TDocuments,
TDocumentType extends TDocument["type"] = TDocument["type"],
>(
documentType: TDocumentType,
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<ExtractDocumentType<TDocument, TDocumentType>> {
return await this.getFirst<ExtractDocumentType<TDocument, TDocumentType>>(
appendFilters(params, filter.at("document.type", documentType)),
)
}
/**
* Fetches pages with a specific type. Results are paginated.
*
* @example
*
* ```ts
* const response = await client.getByType("blog_post")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbytype}
*/
async getByType<
TDocument extends TDocuments,
TDocumentType extends TDocument["type"] = TDocument["type"],
>(
documentType: TDocumentType,
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<ExtractDocumentType<TDocument, TDocumentType>>> {
return await this.get<ExtractDocumentType<TDocument, TDocumentType>>(
appendFilters(params, filter.at("document.type", documentType)),
)
}
/**
* Fetches pages with a specific type. This method may make multiple network
* requests to fetch all matching documents.
*
* @example
*
* ```ts
* const pages = await client.getAllByType("blog_post")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getallbytype}
*/
async getAllByType<
TDocument extends TDocuments,
TDocumentType extends TDocument["type"] = TDocument["type"],
>(
documentType: TDocumentType,
params?: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams,
): Promise<ExtractDocumentType<TDocument, TDocumentType>[]> {
return await this.dangerouslyGetAll<
ExtractDocumentType<TDocument, TDocumentType>
>(appendFilters(params, filter.at("document.type", documentType)))
}
/**
* Fetches pages with a specific tag. Results are paginated.
*
* @example
*
* ```ts
* const response = await client.getByTag("featured")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbytag}
*/
async getByTag<TDocument extends TDocuments>(
tag: string,
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<TDocument>> {
return await this.get<TDocument>(
appendFilters(params, filter.any("document.tags", [tag])),
)
}
/**
* Fetches pages with a specific tag. This method may make multiple network
* requests to fetch all matching documents.
*
* @example
*
* ```ts
* const pages = await client.getAllByTag("featured")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getallbytag}
*/
async getAllByTag<TDocument extends TDocuments>(
tag: string,
params?: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams,
): Promise<TDocument[]> {
return await this.dangerouslyGetAll<TDocument>(
appendFilters(params, filter.any("document.tags", [tag])),
)
}
/**
* Fetches pages with every tag from a list of tags. Results are paginated.
*
* @example
*
* ```ts
* const response = await client.getByEveryTag(["featured", "homepage"])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbyeverytag}
*/
async getByEveryTag<TDocument extends TDocuments>(
tags: string[],
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<TDocument>> {
return await this.get<TDocument>(
appendFilters(params, filter.at("document.tags", tags)),
)
}
/**
* Fetches pages with every tag from a list of tags. This method may make
* multiple network requests to fetch all matching pages.
*
* @example
*
* ```ts
* const pages = await client.getAllByEveryTag(["featured", "homepage"])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getallbyeverytag}
*/
async getAllByEveryTag<TDocument extends TDocuments>(
tags: string[],
params?: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams,
): Promise<TDocument[]> {
return await this.dangerouslyGetAll<TDocument>(
appendFilters(params, filter.at("document.tags", tags)),
)
}
/**
* Fetches pages with at least one tag from a list of tags. Results are
* paginated.
*
* @example
*
* ```ts
* const response = await client.getBySomeTags(["featured", "homepage"])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getbysometags}
*/
async getBySomeTags<TDocument extends TDocuments>(
tags: string[],
params?: Partial<BuildQueryURLArgs> & FetchParams,
): Promise<Query<TDocument>> {
return await this.get<TDocument>(
appendFilters(params, filter.any("document.tags", tags)),
)
}
/**
* Fetches pages with at least one tag from a list of tags. This method may
* make multiple network requests to fetch all matching documents.
*
* @example
*
* ```ts
* const pages = await client.getAllBySomeTags(["featured", "homepage"])
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getallbysometags}
*/
async getAllBySomeTags<TDocument extends TDocuments>(
tags: string[],
params?: Partial<Omit<BuildQueryURLArgs, "page">> &
GetAllParams &
FetchParams,
): Promise<TDocument[]> {
return await this.dangerouslyGetAll<TDocument>(
appendFilters(params, filter.any("document.tags", tags)),
)
}
/**
* Fetches metadata about the client's Prismic repository.
*
* @example
*
* ```ts
* const repository = await client.getRepository()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getrepository}
*/
async getRepository(
params?: Pick<BuildQueryURLArgs, "accessToken"> & FetchParams,
): Promise<Repository> {
if (
this.#cachedRepository &&
this.#cachedRepositoryExpiration > Date.now()
) {
return this.#cachedRepository
}
const url = new URL(this.documentAPIEndpoint)
const accessToken = params?.accessToken || this.accessToken
if (accessToken) {
url.searchParams.set("access_token", accessToken)
}
const response = await this.#request(url, params)
if (response.ok) {
this.#cachedRepository = (await response.json()) as Repository
this.#cachedRepositoryExpiration = Date.now() + REPOSITORY_CACHE_TTL
return this.#cachedRepository
}
if (response.status === 404) {
throw new RepositoryNotFoundError(
`Prismic repository not found. Check that "${this.documentAPIEndpoint}" is pointing to the correct repository.`,
url.toString(),
undefined,
)
}
return await this.#throwContentAPIError(response, url.toString())
}
/**
* Fetches the repository's active refs.
*
* @example
*
* ```ts
* const refs = await client.getRefs()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getrefs}
*/
async getRefs(params?: FetchParams): Promise<Ref[]> {
const repository = await this.getRepository(params)
return repository.refs
}
/**
* Fetches a ref by its ID.
*
* @example
*
* ```ts
* const ref = await client.getRefByID("YhE3YhEAACIA4321")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getrefbyid}
*/
async getRefByID(id: string, params?: FetchParams): Promise<Ref> {
const refs = await this.getRefs(params)
const ref = refs.find((ref) => ref.id === id)
if (!ref) {
throw new PrismicError(
`Ref with ID "${id}" could not be found.`,
undefined,
undefined,
)
}
return ref
}
/**
* Fetches a ref by its label. A release ref's label is its name shown in the
* Page Builder.
*
* @example
*
* ```ts
* const ref = await client.getRefByLabel("My Release")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getrefbylabel}
*/
async getRefByLabel(label: string, params?: FetchParams): Promise<Ref> {
const refs = await this.getRefs(params)
const ref = refs.find((ref) => ref.label === label)
if (!ref) {
throw new PrismicError(
`Ref with label "${label}" could not be found.`,
undefined,
undefined,
)
}
return ref
}
/**
* Fetches the repository's master ref.
*
* @example
*
* ```ts
* const masterRef = await client.getMasterRef()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getmasterref}
*/
async getMasterRef(params?: FetchParams): Promise<Ref> {
const refs = await this.getRefs(params)
const ref = refs.find((ref) => ref.isMasterRef)
if (!ref) {
throw new PrismicError(
"Master ref could not be found.",
undefined,
undefined,
)
}
return ref
}
/**
* Fetches the repository's active releases.
*
* @example
*
* ```ts
* const releases = await client.getReleases()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getreleases}
*/
async getReleases(params?: FetchParams): Promise<Ref[]> {
const refs = await this.getRefs(params)
return refs.filter((ref) => !ref.isMasterRef)
}
/**
* Fetches a release with a specific ID.
*
* @example
*
* ```ts
* const release = await client.getReleaseByID("YhE3YhEAACIA4321")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getreleasebyid}
*/
async getReleaseByID(id: string, params?: FetchParams): Promise<Ref> {
const releases = await this.getReleases(params)
const release = releases.find((ref) => ref.id === id)
if (!release) {
throw new PrismicError(
`Release with ID "${id}" could not be found.`,
undefined,
undefined,
)
}
return release
}
/**
* Fetches a release by its label. A release ref's label is its name shown in
* the Page Builder.
*
* @example
*
* ```ts
* const release = await client.getReleaseByLabel("My Release")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#getreleasebylabel}
*/
async getReleaseByLabel(label: string, params?: FetchParams): Promise<Ref> {
const releases = await this.getReleases(params)
const release = releases.find((ref) => ref.label === label)
if (!release) {
throw new PrismicError(
`Release with label "${label}" could not be found.`,
undefined,
undefined,
)
}
return release
}
/**
* Fetches the repository's page tags.
*
* @example
*
* ```ts
* const tags = await client.getTags()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#gettags}
*/
async getTags(params?: FetchParams): Promise<string[]> {
const repository = await this.getRepository(params)
const form = repository.forms.tags
if (form) {
const url = new URL(form.action)
if (this.accessToken) {
url.searchParams.set("access_token", this.accessToken)
}
const response = await this.#request(url, params)
if (response.ok) {
return (await response.json()) as string[]
}
}
return repository.tags
}
/**
* Builds a Content API query URL with a set of parameters.
*
* @example
*
* ```ts
* const url = await client.buildQueryURL({
* filters: [filter.at("document.type", "blog_post")],
* })
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#buildqueryurl}
*/
async buildQueryURL({
signal,
fetchOptions,
...params
}: Partial<BuildQueryURLArgs> & FetchParams = {}): Promise<string> {
const ref =
params.ref ||
(await this.#getResolvedRef({
accessToken: params.accessToken,
signal,
fetchOptions,
}))
const integrationFieldsRef =
params.integrationFieldsRef ||
(
await this.getRepository({
accessToken: params.accessToken,
signal,
fetchOptions,
})
).integrationFieldsRef ||
undefined
return buildQueryURL(this.documentAPIEndpoint, {
...this.defaultParams,
...params,
ref,
integrationFieldsRef,
routes: params.routes || this.routes,
brokenRoute: params.brokenRoute || this.brokenRoute,
accessToken: params.accessToken || this.accessToken,
})
}
/**
* Fetches a previewed page's URL using a preview token and page ID.
*
* @example
*
* ```ts
* const url = await client.resolvePreviewURL({
* linkResolver,
* defaultURL: "/",
* })
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#resolvepreviewurl}
*/
async resolvePreviewURL<LinkResolverReturnType>(
args: {
/** A function converts a document to a URL in your website. */
linkResolver?: LinkResolverFunction<LinkResolverReturnType>
/** A fallback URL used when the document does not have a URL. */
defaultURL: string
/** The preview token for the preview session. */
previewToken?: string
/** The previewed document's ID. */
documentID?: string
} & FetchParams,
): Promise<string> {
let documentID: string | undefined | null = args.documentID
let previewToken: string | undefined | null = args.previewToken
if (typeof globalThis.location !== "undefined") {
const searchParams = new URLSearchParams(globalThis.location.search)
documentID = documentID || searchParams.get("documentId")
previewToken = previewToken || searchParams.get("token")
} else if (this.#autoPreviewsRequest) {
if ("query" in this.#autoPreviewsRequest) {
documentID =
documentID || (this.#autoPreviewsRequest.query?.documentId as string)
previewToken =
previewToken || (this.#autoPreviewsRequest.query?.token as string)
} else if (
"url" in this.#autoPreviewsRequest &&
this.#autoPreviewsRequest.url
) {
// Including "missing-host://" by default
// handles a case where Next.js Route Handlers
// only provide the pathname and search
// parameters in the `url` property
// (e.g. `/api/preview?foo=bar`).
const searchParams = new URL(
this.#autoPreviewsRequest.url,
"missing-host://",
).searchParams
documentID = documentID || searchParams.get("documentId")
previewToken = previewToken || searchParams.get("token")
}
}
if (documentID != null && previewToken != null) {
const document = await this.getByID(documentID, {
ref: previewToken,
lang: "*",
signal: args.signal,
fetchOptions: args.fetchOptions,
})
const url = asLink(document, { linkResolver: args.linkResolver })
if (typeof url === "string") {
return url
}
}
return args.defaultURL
}
/**
* Configures the client to query the latest published content. This is the
* client's default mode.
*
* @example
*
* ```ts
* client.queryLatestContent()
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#querylatestcontent}
*/
queryLatestContent(): void {
this.#getRef = undefined
}
/**
* Configures the client to query content from a release with a specific ID.
*
* @example
*
* ```ts
* client.queryContentFromReleaseByID("YhE3YhEAACIA4321")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#querycontentfromreleasebyid}
*/
queryContentFromReleaseByID(id: string): void {
this.#getRef = async (params) => {
const release = await this.getReleaseByID(id, params)
return release.ref
}
}
/**
* Configures the client to query content from a release with a specific
* label.
*
* @example
*
* ```ts
* client.queryContentFromReleaseByLabel("My Release")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#querycontentfromreleasebylabel}
*/
queryContentFromReleaseByLabel(label: string): void {
this.#getRef = async (params) => {
const release = await this.getReleaseByLabel(label, params)
return release.ref
}
}
/**
* Configures the client to query content from a specific ref.
*
* @example
*
* ```ts
* client.queryContentFromRef("my-ref")
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#querycontentfromref}
*/
queryContentFromRef(ref: string | GetRef): void {
this.#getRef = typeof ref === "string" ? () => ref : ref
}
/**
* A preconfigured `fetch()` function for Prismic's GraphQL API that can be
* provided to GraphQL clients.
*
* @example
*
* ```ts
* import { createClient, getGraphQLEndpoint } from "@prismicio/client"
*
* const client = createClient("example-prismic-repo")
* const graphQLClient = new ApolloClient({
* link: new HttpLink({
* uri: getGraphQLEndpoint(client.repositoryName),
* // Provide `client.graphQLFetch` as the fetch implementation.
* fetch: client.graphQLFetch,
* // Using GET is required.
* useGETForQueries: true,
* }),
* cache: new InMemoryCache(),
* })
* ```
*
* @see {@link https://prismic.io/docs/technical-reference/prismicio-client/v7#graphqlfetch}
*/
async graphQLFetch(
input: RequestInfo,
init?: Omit<RequestInit, "signal"> & { signal?: AbortSignalLike },
): Promise<Response> {
const params = {
accessToken: this.accessToken,
fetchOptions: this.fetchOptions,
}
const repository = await this.getRepository(params)
const ref = await this.#getResolvedRef(params)
const headers: NonNullable<RequestInitLike["headers"]> = {}
headers["prismic-ref"] = ref
if (this.accessToken) {
headers["authorization"] = `Token ${this.accessToken}`
}
if (repository.integrationFieldsRef) {
headers["prismic-integration-field-ref"] = repository.integrationFieldsRef
}
for (const [key, value] of Object.entries(init?.headers ?? {})) {
headers[key.toLowerCase()] = value
}
const url = new URL(typeof input === "string" ? input : input.url)
const query = (url.searchParams.get("query") ?? "").replace(
// Minify the query
/(\n| )*( |{|})(\n| )*/gm,
(_chars, _spaces, brackets) => brackets,
)
url.searchParams.set("query", query)
// Only used to prevent caching; caches ignore header differences
url.searchParams.set("ref", ref)
return (await this.fetchFn(url.toString(), {
...init,
headers,
})) as Response
}
/**
* Returns the ref needed to query based on the client's current state. This
* method may make a network request to fetch a ref or resolve the user's ref
* thunk.
*
* If auto previews are enabled, the preview ref takes priority.
*
* The following strategies are used depending on the client's state:
*
* - If the user called `queryLatestContent`: Use the repository's master ref.
* The ref is cached for 5 seconds. After 5 seconds, a new master ref is
* fetched.
* - If the user called `queryContentFromReleaseByID`: Use the release's ref.
* The ref is cached for 5 seconds. After 5 seconds, a new ref for the
* release is fetched.
* - If the user called `queryContentFromReleaseByLabel`: Use the release's ref.
* The ref is cached for 5 seconds. After 5 seconds, a new ref for the
* release is fetched.
* - If the user called `queryContentFromRef`: Use the provided ref. Fall back
* to the master ref if the ref is not a string.
*/
async #getResolvedRef(
params?: Pick<BuildQueryURLArgs, "accessToken"> & FetchParams,
) {
if (this.#autoPreviews) {
const cookies = this.#autoPreviewsRequest?.headers
? "get" in this.#autoPreviewsRequest.headers
? this.#autoPreviewsRequest.headers.get("cookie")
: this.#autoPreviewsRequest.headers.cookie
: globalThis.document?.cookie
const previewRef = getPreviewCookie(cookies ?? "")
if (previewRef) {
return previewRef
}
}
const ref = await this.#getRef?.(params)
if (ref) {
return ref
}
const masterRef = await this.getMasterRef(params)
return masterRef.ref
}
/**
* Performs a low-level Content API request with the given parameters.
* Automatically retries if an invalid ref is used.
*/
async #internalGet(
params?: Partial<BuildQueryURLArgs> & FetchParams,
attempt = 1,
): Promise<ResponseLike> {
const url = await this.buildQueryURL(params)
const response = await this.#request(new URL(url), params)
if (response.ok) {
return response
}
try {
return await this.#throwContentAPIError(response, url)
} catch (error) {
if (
(error instanceof RefNotFoundError ||
error instanceof RefExpiredError) &&
attempt < MAX_INVALID_REF_RETRY_ATTEMPTS
) {
// If no explicit ref is given (i.e. the master ref from
// /api/v2 is used), clear the cached repository value.
// Clearing the cached value prevents other methods from
// using a known-stale ref.
if (!params?.ref) {
this.#cachedRepository = undefined
}
const masterRef = error.message.match(/master ref is: (?<ref>.*)$/i)
?.groups?.ref
if (!masterRef) {
throw error
}
const badRef = new URL(url).searchParams.get("ref")
const issue = error instanceof RefNotFoundError ? "invalid" : "expired"
throttledWarn(
`[/client] The ref (${badRef}) was ${issue}. Now retrying with the latest master ref (${masterRef}). If you were previewing content, the response will not include draft content.`,
)
return await this.#internalGet(
{ ...params, ref: masterRef },
attempt + 1,
)
}
throw error
}
}
/**
* Throws an error based on a Content API response. Only call in known-errored
* states.
*/
async #throwContentAPIError(
response: ResponseLike,
url: string,
): Promise<never> {
switch (response.status) {
case 400: {
const json = await response.clone().json()
throw new ParsingError(json.message, url, json)
}
case 401: {
const json = await response.clone().json()
throw new ForbiddenError(json.message, url, json)
}
case 404: {
const json = await response.clone().json()
switch (json.type) {
case "api_notfound_error": {
throw new RefNotFoundError(json.message, url, json)
}
case "api_security_error": {
if (/preview token.*expired/i.test(json.message)) {
throw new PreviewTokenExpiredError(json.message, url, json)
}
}
default: {
throw new NotFoundError(json.message, url, json)
}
}
}
case 410: {
const json = await response.clone().json()
throw new RefExpiredError(json.message, url, json)
}
default: {
throw new PrismicError(undefined, url, await response.text())
}
}
}
/** Performs a low-level network request with the client's fetch options. */
async #request(url: URL, params?: FetchParams): Promise<ResponseLike> {
return await request(
url,
{
...this.fetchOptions,
...params?.fetchOptions,
headers: {
...this.fetchOptions?.headers,
...params?.fetchOptions?.headers,
},
signal:
params?.fetchOptions?.signal ||
params?.signal ||
this.fetchOptions?.signal,
},
this.fetchFn,
)
}
}
/** Appends filters to a params object. */
function appendFilters<T extends Pick<BuildQueryURLArgs, "filters">>(
params = {} as T,
...filters: string[]
): T & { filters: string[] } {
return { ...params, filters: [...(params.filters ?? []), ...filters] }
}