UNPKG

@prismicio/client

Version:

The official JavaScript + TypeScript client library for Prismic

1,694 lines (1,557 loc) 56.2 kB
import { appendFilters } from "./lib/appendFilters" import { castThunk } from "./lib/castThunk" import { devMsg } from "./lib/devMsg" import { everyTagFilter } from "./lib/everyTagFilter" import { findMasterRef } from "./lib/findMasterRef" import { findRefByID } from "./lib/findRefByID" import { findRefByLabel } from "./lib/findRefByLabel" import { getPreviewCookie } from "./lib/getPreviewCookie" import { minifyGraphQLQuery } from "./lib/minifyGraphQLQuery" import type { ResponseLike } from "./lib/request" import { type AbortSignalLike, type FetchLike, type RequestInitLike, request, } from "./lib/request" import { someTagsFilter } from "./lib/someTagsFilter" import { throttledLog } from "./lib/throttledLog" import { typeFilter } from "./lib/typeFilter" 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 } from "./errors/ForbiddenError" import { NotFoundError } from "./errors/NotFoundError" import { ParsingError } from "./errors/ParsingError" import { PreviewTokenExpiredError } from "./errors/PreviewTokenExpired" import { PrismicError } from "./errors/PrismicError" import { RefExpiredError } from "./errors/RefExpiredError" import { RefNotFoundError } from "./errors/RefNotFoundError" import { RepositoryNotFoundError } from "./errors/RepositoryNotFoundError" import type { LinkResolverFunction } from "./helpers/asLink" import { asLink } from "./helpers/asLink" import type { BuildQueryURLArgs } from "./buildQueryURL" import { buildQueryURL } from "./buildQueryURL" import { filter } from "./filter" import { getRepositoryEndpoint } from "./getRepositoryEndpoint" import { getRepositoryName } from "./getRepositoryName" import { isRepositoryEndpoint } from "./isRepositoryEndpoint" /** * The largest page size allowed by the Prismic REST API V2. This value is used * to minimize the number of requests required to query content. */ const MAX_PAGE_SIZE = 100 /** * The number of milliseconds in which repository metadata is considered valid. * A ref can be invalidated quickly depending on how frequently content is * updated in the Prismic repository. As such, repository's metadata can only be * considered valid for a short amount of time. */ export const REPOSITORY_CACHE_TTL = 5000 /** * The number of milliseconds in which a multi-page `getAll` (e.g. `getAll`, * `getAllByType`, `getAllByTag`) will wait between individual page requests. * * This is done to ensure API performance is sustainable and reduces the chance * of a failed API request due to overloading. */ export const GET_ALL_QUERY_DELAY = 500 /** * The maximum number of attempts to retry a query with an invalid ref. We allow * multiple attempts since each attempt may use a different (and possibly * invalid) ref. Capping the number of attempts prevents infinite loops. */ const MAX_INVALID_REF_RETRY_ATTEMPTS = 3 /** * Extracts one or more Prismic document types that match a given Prismic * document type. If no matches are found, no extraction is performed and the * union of all provided Prismic document types are returned. * * @typeParam TDocuments - Prismic document types from which to extract. * @typeParam TDocumentType - Type(s) to match `TDocuments` against. */ type ExtractDocumentType< TDocuments extends PrismicDocument, TDocumentType extends TDocuments["type"], > = Extract<TDocuments, { type: TDocumentType }> extends never ? TDocuments : Extract<TDocuments, { type: TDocumentType }> /** * Modes for client ref management. */ enum RefStateMode { /** * Use the repository's master ref. */ Master = "Master", /** * Use a given Release identified by its ID. */ ReleaseID = "ReleaseID", /** * Use a given Release identified by its label. */ ReleaseLabel = "ReleaseLabel", /** * Use a given ref. */ Manual = "Manual", } /** * The minimum required properties to treat as an HTTP Request for automatic * Prismic preview support. */ export type HttpRequestLike = | /** * Web API Request * * @see http://developer.mozilla.org/en-US/docs/Web/API/Request */ { headers?: { get(name: string): string | null } url?: string } /** * Express-style Request */ | { headers?: { cookie?: string } query?: Record<string, unknown> } /** * An object containing stateful information about a client's ref strategy. */ type RefState = { /** * Determines if automatic preview support is enabled. */ autoPreviewsEnabled: boolean /** * An optional HTTP server request object used during previews if automatic * previews are enabled. */ httpRequest?: HttpRequestLike } & ( | { mode: RefStateMode.Master } | { mode: RefStateMode.ReleaseID releaseID: string } | { mode: RefStateMode.ReleaseLabel releaseLabel: string } | { mode: RefStateMode.Manual ref: RefStringOrThunk } ) /** * A ref or a function that returns a ref. If a static ref is known, one can be * given. If the ref must be fetched on-demand, a function can be provided. This * function can optionally be asynchronous. */ type RefStringOrThunk = | string | (() => string | undefined | Promise<string | undefined>) /** * Parameters for any client method 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 /** * An `AbortSignal` provided by an `AbortController`. This allows the network * request to be cancelled if necessary. * * @deprecated Move the `signal` parameter into `fetchOptions.signal`: * * @see \<https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal\> */ signal?: AbortSignalLike } /** * Configuration for clients that determine how content is queried. */ export type ClientConfig = { /** * The full Rest API V2 endpoint for the repository. This is only helpful if * you're using Prismic behind a proxy which we do not recommend. * * @defaultValue `getRepositoryEndpoint(repositoryNameOrEndpoint)` */ documentAPIEndpoint?: string /** * The secure token for accessing the Prismic repository. This is only * required if the repository is set to private. */ accessToken?: string /** * A string representing a version of the Prismic repository's content. This * may point to the latest version (called the "master ref"), or a preview * with draft content. */ ref?: RefStringOrThunk /** * A list of route resolver objects that define how a document's `url` * property is resolved. * * {@link https://prismic.io/docs/route-resolver} */ routes?: NonNullable<BuildQueryURLArgs["routes"]> /** * The `brokenRoute` option allows you to define the route populated in the * `url` property for broken link or content relationship fields. A broken * link is a link or content relationship field whose linked document has been * unpublished or deleted. * * {@link https://prismic.io/docs/route-resolver} */ brokenRoute?: NonNullable<BuildQueryURLArgs["brokenRoute"]> /** * Default parameters that will be sent with each query. These parameters can * be overridden on each query if needed. */ defaultParams?: Omit< BuildQueryURLArgs, "ref" | "integrationFieldsRef" | "accessToken" | "routes" | "brokenRoute" > /** * The function used to make network requests to the Prismic REST 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 * overriden on a per-query basis using the query's `fetchOptions` parameter. */ fetchOptions?: RequestInitLike } /** * Parameters specific to client methods that fetch all documents. These methods * start with `getAll` (for example, `getAllByType`). */ type GetAllParams = { /** * Limit the number of documents queried. If a number is not provided, there * will be no limit and all matching documents will be returned. */ limit?: number } /** * Arguments to determine how the URL for a preview session is resolved. */ type ResolvePreviewArgs<LinkResolverReturnType> = { /** * A function that maps a Prismic document to a URL within your app. */ linkResolver?: LinkResolverFunction<LinkResolverReturnType> /** * A fallback URL if the link resolver does not return a value. */ defaultURL: string /** * The preview token (also known as a ref) that will be used to query preview * content from the Prismic repository. */ previewToken?: string /** * The previewed document that will be used to determine the destination URL. */ documentID?: string } /** * A client that allows querying content from a Prismic repository. * * If used in an environment where a global `fetch` function is unavailable, * such as Node.js, the `fetch` option must be provided as part of the `options` * parameter. * * @typeParam TDocuments - Document types that are registered for the Prismic * repository. Query methods will automatically be typed based on this type. */ export class Client<TDocuments extends PrismicDocument = PrismicDocument> { /** * The function used to make network requests to the Prismic REST API. In * environments where a global `fetch` function does not exist, such as * Node.js, this function must be provided. */ fetchFn: FetchLike fetchOptions?: RequestInitLike #repositoryName: string | undefined /** * 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 } /** * The Prismic REST API V2 endpoint for the repository (use * `prismic.getRepositoryEndpoint` for the default endpoint). */ documentAPIEndpoint: string /** * The Prismic REST API V2 endpoint for the repository (use * `prismic.getRepositoryEndpoint` for the default endpoint). * * @deprecated Use `documentAPIEndpoint` instead. */ // TODO: Remove in v8. set endpoint(value: string) { this.documentAPIEndpoint = value } /** * The Prismic REST API V2 endpoint for the repository (use * `prismic.getRepositoryEndpoint` for the default endpoint). * * @deprecated Use `documentAPIEndpoint` instead. */ // TODO: Remove in v8. get endpoint(): string { return this.documentAPIEndpoint } /** * The secure token for accessing the API (only needed if your repository is * set to private). * * {@link https://user-guides.prismic.io/en/articles/1036153-generating-an-access-token} */ accessToken?: string /** * A list of route resolver objects that define how a document's `url` field * is resolved. * * {@link https://prismic.io/docs/route-resolver} */ routes?: NonNullable<BuildQueryURLArgs["routes"]> /** * The `brokenRoute` option allows you to define the route populated in the * `url` property for broken link or content relationship fields. A broken * link is a link or content relationship field whose linked document has been * unpublished or deleted. * * {@link https://prismic.io/docs/route-resolver} */ brokenRoute?: NonNullable<BuildQueryURLArgs["brokenRoute"]> /** * Default parameters that will be sent with each query. These parameters can * be overridden on each query if needed. */ defaultParams?: Omit< BuildQueryURLArgs, "ref" | "integrationFieldsRef" | "accessToken" | "routes" > /** * The client's ref mode state. This determines which ref is used during * queries. */ private refState: RefState = { mode: RefStateMode.Master, autoPreviewsEnabled: true, } /** * Cached repository value. */ private cachedRepository: Repository | undefined /** * Timestamp at which the cached repository data is considered stale. */ private cachedRepositoryExpiration = 0 /** * Creates a Prismic client that can be used to query a repository. * * If used in an environment where a global `fetch` function is unavailable, * such as in some Node.js versions, the `fetch` option must be provided as * part of the `options` parameter. * * @param repositoryNameOrEndpoint - The Prismic repository name or full Rest * API V2 endpoint for the repository. * @param options - Configuration that determines how content will be queried * from the Prismic repository. * * @returns A client that can query content from the repository. */ constructor(repositoryNameOrEndpoint: string, options: ClientConfig = {}) { this.fetchOptions = options.fetchOptions if (typeof options.fetch === "function") { this.fetchFn = options.fetch } else if (typeof globalThis.fetch === "function") { this.fetchFn = globalThis.fetch as FetchLike } else { throw new PrismicError( "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` option.", undefined, undefined, ) } // 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) } if ( (options.documentAPIEndpoint || isRepositoryEndpoint(repositoryNameOrEndpoint)) && process.env.NODE_ENV === "development" ) { const documentAPIEndpoint = options.documentAPIEndpoint || repositoryNameOrEndpoint // Matches non-API v2 `.prismic.io` endpoints, see: https://regex101.com/r/xRsavu/1 if (/\.prismic\.io\/(?!api\/v2\/?)/i.test(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, ) } const hostname = new URL(documentAPIEndpoint).hostname.toLowerCase() // Matches non-.cdn `.prismic.io` endpoints if ( hostname.endsWith(".prismic.io") && !hostname.endsWith(".cdn.prismic.io") ) { console.warn( `[@prismicio/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")}`, ) } // Warn if the user provided both a repository endpoint and an `documentAPIEndpoint` and they are different if ( options.documentAPIEndpoint && isRepositoryEndpoint(repositoryNameOrEndpoint) && repositoryNameOrEndpoint !== options.documentAPIEndpoint ) { console.warn( `[@prismicio/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 (isRepositoryEndpoint(repositoryNameOrEndpoint)) { this.documentAPIEndpoint = repositoryNameOrEndpoint try { this.repositoryName = getRepositoryName(repositoryNameOrEndpoint) } catch (error) { console.warn( `[@prismicio/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")}`, ) } } else { this.documentAPIEndpoint = options.documentAPIEndpoint || getRepositoryEndpoint(repositoryNameOrEndpoint) this.repositoryName = repositoryNameOrEndpoint } this.accessToken = options.accessToken this.routes = options.routes this.brokenRoute = options.brokenRoute this.defaultParams = options.defaultParams if (options.ref) { this.queryContentFromRef(options.ref) } this.graphQLFetch = this.graphQLFetch.bind(this) } /** * Enables the client to automatically query content from a preview session if * one is active in browser environments. This is enabled by default in the * browser. * * For server environments, use `enableAutoPreviewsFromReq`. * * @example * * ```ts * client.enableAutoPreviews() * ``` * * @see enableAutoPreviewsFromReq */ enableAutoPreviews(): void { this.refState.autoPreviewsEnabled = true } /** * Enables the client to automatically query content from a preview session if * one is active in server environments. This is disabled by default on the * server. * * For browser environments, use `enableAutoPreviews`. * * @example * * ```ts * // In an express app * app.get("/", function (req, res) { * client.enableAutoPreviewsFromReq(req) * }) * ``` * * @param req - An HTTP server request object containing the request's * cookies. */ enableAutoPreviewsFromReq<R extends HttpRequestLike>(req: R): void { this.refState.httpRequest = req this.refState.autoPreviewsEnabled = true } /** * Disables the client from automatically querying content from a preview * session if one is active. * * Automatic preview content querying is enabled by default unless this method * is called. * * @example * * ```ts * client.disableAutoPreviews() * ``` */ disableAutoPreviews(): void { this.refState.autoPreviewsEnabled = false } /** * Queries content from the Prismic repository. * * @example * * ```ts * const response = await client.get() * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param params - Parameters to filter, sort, and paginate results. * * @returns A paginated response containing the result of the query. */ async get<TDocument extends TDocuments>( params?: Partial<BuildQueryURLArgs> & FetchParams, ): Promise<Query<TDocument>> { const { data } = await this._get<TDocument>(params) return data } /** * Queries content from the Prismic repository and returns only the first * result, if any. * * @example * * ```ts * const document = await client.getFirst() * ``` * * @typeParam TDocument - Type of the Prismic document returned. * * @param params - Parameters to filter, sort, and paginate results. * * @returns The first result of the query, if any. */ async getFirst<TDocument extends TDocuments>( params?: Partial<BuildQueryURLArgs> & FetchParams, ): Promise<TDocument> { const actualParams = { ...params } if (!(params && params.page) && !params?.pageSize) { actualParams.pageSize = this.defaultParams?.pageSize ?? 1 } const { data, url } = await this._get<TDocument>(actualParams) const firstResult = data.results[0] if (firstResult) { return firstResult } throw new NotFoundError("No documents were returned", url, undefined) } /** * **IMPORTANT**: Avoid using `dangerouslyGetAll` as it may be slower and * require more resources than other methods. Prefer using other methods that * filter by filters such as `getAllByType`. * * Queries content from the Prismic repository and returns all matching * content. If no filters are provided, all documents will be fetched. * * This method may make multiple network requests to query all matching * content. * * @example * * ```ts * const response = await client.dangerouslyGetAll() * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param params - Parameters to filter, sort, and paginate results. * * @returns A list of documents matching the query. */ 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) } /** * Queries a document from the Prismic repository with a specific ID. * * @remarks * A document's UID is different from its ID. An ID is automatically generated * for all documents and is made available on its `id` property. A UID is * provided in the Prismic editor and is unique among all documents of its * custom type. * * @example * * ```ts * const document = await client.getByID("WW4bKScAAMAqmluX") * ``` * * @typeParam TDocument- Type of the Prismic document returned. * * @param id - ID of the document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns The document with an ID matching the `id` parameter, if a matching * document exists. */ 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)), ) } /** * Queries documents from the Prismic repository with specific IDs. * * @remarks * A document's UID is different from its ID. An ID is automatically generated * for all documents and is made available on its `id` property. A UID is * provided in the Prismic editor and is unique among all documents of its * custom type. * * @example * * ```ts * const response = await client.getByIDs([ * "WW4bKScAAMAqmluX", * "U1kTRgEAAC8A5ldS", * ]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param ids - A list of document IDs. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A paginated response containing documents with IDs matching the * `ids` parameter. */ 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)), ) } /** * Queries all documents from the Prismic repository with specific IDs. * * This method may make multiple network requests to query all matching * content. * * @remarks * A document's UID is different from its ID. An ID is automatically generated * for all documents and is made available on its `id` property. A UID is * provided in the Prismic editor and is unique among all documents of its * custom type. * * @example * * ```ts * const response = await client.getAllByIDs([ * "WW4bKScAAMAqmluX", * "U1kTRgEAAC8A5ldS", * ]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param ids - A list of document IDs. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A list of documents with IDs matching the `ids` parameter. */ 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)), ) } /** * Queries a document from the Prismic repository with a specific UID and * custom type. * * @remarks * A document's UID is different from its ID. An ID is automatically generated * for all documents and is made available on its `id` property. A UID is * provided in the Prismic editor and is unique among all documents of its * custom type. * * @example * * ```ts * const document = await client.getByUID("blog_post", "my-first-post") * ``` * * @typeParam TDocument - Type of the Prismic document returned. * * @param documentType - The API ID of the document's custom type. * @param uid - UID of the document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns The document with a UID matching the `uid` parameter, if a * matching document exists. */ 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, [ typeFilter(documentType), filter.at(`my.${documentType}.uid`, uid), ]), ) } /** * Queries document from the Prismic repository with specific UIDs and Custom * Type. * * @remarks * A document's UID is different from its ID. An ID is automatically generated * for all documents and is made available on its `id` property. A UID is * provided in the Prismic editor and is unique among all documents of its * custom type. * * @example * * ```ts * const document = await client.getByUIDs("blog_post", [ * "my-first-post", * "my-second-post", * ]) * ``` * * @typeParam TDocument - Type of the Prismic document returned. * * @param documentType - The API ID of the document's custom type. * @param uids - A list of document UIDs. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A paginated response containing documents with UIDs matching the * `uids` parameter. */ 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, [ typeFilter(documentType), filter.in(`my.${documentType}.uid`, uids), ]), ) } /** * Queries all documents from the Prismic repository with specific UIDs and * custom type. * * This method may make multiple network requests to query all matching * content. * * @remarks * A document's UID is different from its ID. An ID is automatically generated * for all documents and is made available on its `id` property. A UID is * provided in the Prismic editor and is unique among all documents of its * custom type. * * @example * * ```ts * const response = await client.getAllByUIDs([ * "my-first-post", * "my-second-post", * ]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param documentType - The API ID of the document's custom type. * @param uids - A list of document UIDs. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A list of documents with UIDs matching the `uids` parameter. */ 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, [ typeFilter(documentType), filter.in(`my.${documentType}.uid`, uids), ]), ) } /** * Queries a singleton document from the Prismic repository for a specific * custom type. * * @remarks * A singleton document is one that is configured in Prismic to only allow one * instance. For example, a repository may be configured to contain just one * Settings document. This is in contrast to a repeatable custom type which * allows multiple instances of itself. * * @example * * ```ts * const document = await client.getSingle("settings") * ``` * * @typeParam TDocument - Type of the Prismic document returned. * * @param documentType - The API ID of the singleton custom type. * @param params - Parameters to filter, sort, and paginate the results. * * @returns The singleton document for the custom type, if a matching document * exists. */ 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, typeFilter(documentType)), ) } /** * Queries documents from the Prismic repository for a specific custom type. * * Use `getAllByType` instead if you need to query all documents for a * specific custom type. * * @example * * ```ts * const response = await client.getByType("blog_post") * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param documentType - The API ID of the custom type. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A paginated response containing documents of the custom type. */ 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, typeFilter(documentType)), ) } /** * Queries all documents from the Prismic repository for a specific Custom * Type. * * This method may make multiple network requests to query all matching * content. * * @example * * ```ts * const response = await client.getByType("blog_post") * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param documentType - The API ID of the custom type. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A list of all documents of the custom type. */ 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, typeFilter(documentType))) } /** * Queries documents from the Prismic repository with a specific tag. * * Use `getAllByTag` instead if you need to query all documents with a * specific tag. * * @example * * ```ts * const response = await client.getByTag("food") * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param tag - The tag that must be included on a document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A paginated response containing documents with the tag. */ async getByTag<TDocument extends TDocuments>( tag: string, params?: Partial<BuildQueryURLArgs> & FetchParams, ): Promise<Query<TDocument>> { return await this.get<TDocument>(appendFilters(params, someTagsFilter(tag))) } /** * Queries all documents from the Prismic repository with a specific tag. * * This method may make multiple network requests to query all matching * content. * * @example * * ```ts * const response = await client.getAllByTag("food") * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param tag - The tag that must be included on a document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A list of all documents with the tag. */ async getAllByTag<TDocument extends TDocuments>( tag: string, params?: Partial<Omit<BuildQueryURLArgs, "page">> & GetAllParams & FetchParams, ): Promise<TDocument[]> { return await this.dangerouslyGetAll<TDocument>( appendFilters(params, someTagsFilter(tag)), ) } /** * Queries documents from the Prismic repository with specific tags. A * document must be tagged with all of the queried tags to be included. * * @example * * ```ts * const response = await client.getByEveryTag(["food", "fruit"]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param tags - A list of tags that must be included on a document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A paginated response containing documents with the tags. */ async getByEveryTag<TDocument extends TDocuments>( tags: string[], params?: Partial<BuildQueryURLArgs> & FetchParams, ): Promise<Query<TDocument>> { return await this.get<TDocument>( appendFilters(params, everyTagFilter(tags)), ) } /** * Queries documents from the Prismic repository with specific tags. A * document must be tagged with all of the queried tags to be included. * * This method may make multiple network requests to query all matching * content. * * @example * * ```ts * const response = await client.getAllByEveryTag(["food", "fruit"]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param tags - A list of tags that must be included on a document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A list of all documents with the tags. */ async getAllByEveryTag<TDocument extends TDocuments>( tags: string[], params?: Partial<Omit<BuildQueryURLArgs, "page">> & GetAllParams & FetchParams, ): Promise<TDocument[]> { return await this.dangerouslyGetAll<TDocument>( appendFilters(params, everyTagFilter(tags)), ) } /** * Queries documents from the Prismic repository with specific tags. A * document must be tagged with at least one of the queried tags to be * included. * * @example * * ```ts * const response = await client.getByEveryTag(["food", "fruit"]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param tags - A list of tags that must be included on a document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A paginated response containing documents with at least one of the * tags. */ async getBySomeTags<TDocument extends TDocuments>( tags: string[], params?: Partial<BuildQueryURLArgs> & FetchParams, ): Promise<Query<TDocument>> { return await this.get<TDocument>( appendFilters(params, someTagsFilter(tags)), ) } /** * Queries documents from the Prismic repository with specific tags. A * document must be tagged with at least one of the queried tags to be * included. * * This method may make multiple network requests to query all matching * content. * * @example * * ```ts * const response = await client.getAllBySomeTags(["food", "fruit"]) * ``` * * @typeParam TDocument - Type of Prismic documents returned. * * @param tags - A list of tags that must be included on a document. * @param params - Parameters to filter, sort, and paginate the results. * * @returns A list of all documents with at least one of the tags. */ async getAllBySomeTags<TDocument extends TDocuments>( tags: string[], params?: Partial<Omit<BuildQueryURLArgs, "page">> & GetAllParams & FetchParams, ): Promise<TDocument[]> { return await this.dangerouslyGetAll<TDocument>( appendFilters(params, someTagsFilter(tags)), ) } /** * Returns metadata about the Prismic repository, such as its refs, releases, * and custom types. * * @returns Repository metadata. */ async getRepository(params?: FetchParams): Promise<Repository> { const url = new URL(this.documentAPIEndpoint) if (this.accessToken) { url.searchParams.set("access_token", this.accessToken) } const response = await this.#request(url, params) switch (response.status) { case 200: { return (await response.json()) as Repository } case 401: { const json = await response.json() throw new ForbiddenError(json.error, url.toString(), json) } case 404: { throw new RepositoryNotFoundError( `Prismic repository not found. Check that "${this.documentAPIEndpoint}" is pointing to the correct repository.`, url.toString(), undefined, ) } default: { throw new PrismicError(undefined, url.toString(), await response.text()) } } } /** * Returns a list of all refs for the Prismic repository. * * Refs are used to identify which version of the repository's content should * be queried. All repositories will have at least one ref pointing to the * latest published content called the "master ref". * * @returns A list of all refs for the Prismic repository. */ async getRefs(params?: FetchParams): Promise<Ref[]> { const repository = await this.getRepository(params) return repository.refs } /** * Returns a ref for the Prismic repository with a matching ID. * * @param id - ID of the ref. * * @returns The ref with a matching ID, if it exists. */ async getRefByID(id: string, params?: FetchParams): Promise<Ref> { const refs = await this.getRefs(params) return findRefByID(refs, id) } /** * Returns a ref for the Prismic repository with a matching label. * * @param label - Label of the ref. * * @returns The ref with a matching label, if it exists. */ async getRefByLabel(label: string, params?: FetchParams): Promise<Ref> { const refs = await this.getRefs(params) return findRefByLabel(refs, label) } /** * Returns the master ref for the Prismic repository. The master ref points to * the repository's latest published content. * * @returns The repository's master ref. */ async getMasterRef(params?: FetchParams): Promise<Ref> { const refs = await this.getRefs(params) return findMasterRef(refs) } /** * Returns a list of all Releases for the Prismic repository. Releases are * used to group content changes before publishing. * * @returns A list of all Releases for the Prismic repository. */ async getReleases(params?: FetchParams): Promise<Ref[]> { const refs = await this.getRefs(params) return refs.filter((ref) => !ref.isMasterRef) } /** * Returns a Release for the Prismic repository with a matching ID. * * @param id - ID of the Release. * * @returns The Release with a matching ID, if it exists. */ async getReleaseByID(id: string, params?: FetchParams): Promise<Ref> { const releases = await this.getReleases(params) return findRefByID(releases, id) } /** * Returns a Release for the Prismic repository with a matching label. * * @param label - Label of the ref. * * @returns The ref with a matching label, if it exists. */ async getReleaseByLabel(label: string, params?: FetchParams): Promise<Ref> { const releases = await this.getReleases(params) return findRefByLabel(releases, label) } /** * Returns a list of all tags used in the Prismic repository. * * @returns A list of all tags used in the repository. */ async getTags(params?: FetchParams): Promise<string[]> { const cachedRepository = await this.getCachedRepository(params) const form = cachedRepository.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[] } } const repository = await this.getRepository(params) return repository.tags } /** * Builds a URL used to query content from the Prismic repository. * * @param params - Parameters to filter, sort, and paginate the results. * * @returns A URL string that can be requested to query content. */ async buildQueryURL({ signal, fetchOptions, ...params }: Partial<BuildQueryURLArgs> & FetchParams = {}): Promise<string> { const ref = params.ref || (await this.getResolvedRefString({ signal, fetchOptions })) const integrationFieldsRef = params.integrationFieldsRef || (await this.getCachedRepository({ 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, }) } /** * Determines the URL for a previewed document during an active preview * session. The result of this method should be used to redirect the user to * the document's URL. * * @example * * ```ts * const url = client.resolvePreviewURL({ * linkResolver: (document) => `/${document.uid}` * defaultURL: '/' * }) * ``` * * @param args - Arguments to configure the URL resolving. * * @returns The URL for the previewed document during an active preview * session. The user should be redirected to this URL. */ async resolvePreviewURL<LinkResolverReturnType>( args: ResolvePreviewArgs<LinkResolverReturnType> & 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.refState.httpRequest) { if ("query" in this.refState.httpRequest) { documentID = documentID || (this.refState.httpRequest.query?.documentId as string) previewToken = previewToken || (this.refState.httpRequest.query?.token as string) } else if ( "url" in this.refState.httpRequest && this.refState.httpRequest.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.refState.httpRequest.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 for all future * queries. * * If the `ref` parameter is provided during a query, it takes priority for * that query. * * @example * * ```ts * await client.queryLatestContent() * const document = await client.getByID("WW4bKScAAMAqmluX") * ``` */ queryLatestContent(): void { this.refState.mode = RefStateMode.Master } /** * Configures the client to query content from a specific Release identified * by its ID for all future queries. * * If the `ref` parameter is provided during a query, it takes priority for * that query. * * @example * * ```ts * await client.queryContentFromReleaseByID("YLB7OBAAACMA7Cpa") * const document = await client.getByID("WW4bKScAAMAqmluX") * ``` * * @param releaseID - The ID of the Release. */ queryContentFromReleaseByID(releaseID: string): void { this.refState = { ...this.refState, mode: RefStateMode.ReleaseID, releaseID, } } /** * Configures the client to query content from a specific Release identified * by its label for all future queries. * * If the `ref` parameter is provided during a query, it takes priority for * that query. * * @example * * ```ts * await client.queryContentFromReleaseByLabel("My Release") * const document = await client.getByID("WW4bKScAAMAqmluX") * ``` * * @param releaseLabel - The label of the Release. */ queryContentFromReleaseByLabel(releaseLabel: string): void { this.refState = { ...this.refState, mode: RefStateMode.ReleaseLabel, releaseLabel, } } /** * Configures the client to query content from a specific ref. The ref can be * provided as a string or a function. * * If a function is provided, the ref is fetched lazily before each query. The * function may also be asynchronous. * * @example * * ```ts * await client.queryContentFromRef("my-ref") * const document = await client.getByID("WW4bKScAAMAqmluX") * ``` * * @param ref - The ref or a function that returns the ref from which to query * content. */ queryContentFromRef(ref: RefStringOrThunk): void { this.refState = { ...this.refState, mode: RefStateMode.Manual, ref, } } /** * A `fetch()` function to be used with GraphQL clients configured for * Prismic's GraphQL API. It automatically applies the necessary `prismic-ref` * and Authorization headers. Queries will automatically be minified by * removing whitespace where possible. * * @example * * ```ts * const graphQLClient = new ApolloClient({ * link: new HttpLink({ * uri: prismic.getGraphQLEndpoint(repositoryName), * // Provide `client.graphQLFetch` as the fetch implementation. * fetch: client.graphQLFetch, * // Using GET is required. * useGETForQueries: true, * }), * cache: new InMemoryCache(), * }) * ``` * * @param input - The `fetch()` `input` parameter. Only strings are supported. * @param init - The `fetch()` `init` parameter. Only plain objects are * supported. * * @returns The `fetch()` Response for the request. * * @experimental */ async graphQLFetch( input: RequestInfo, init?: Omit<RequestInit, "signal"> & { signal?: AbortSignalLike }, ): Promise<Response> { const cachedRepository = await this.getCachedRepository() const ref = await this.getResolvedRefString() const unsanitizedHeaders: Record<string, string> = { "Prismic-ref": ref, Authorization: this.accessToken ? `Token ${this.accessToken}` : "", // Asserting `init.headers` is a Record since popular GraphQL // libraries pass this as a Record. Header objects as input // are unsupported. ...(init ? (init.headers as Record<string, string>) : {}), } if (cachedRepository.integrationFieldsRef) { unsanitizedHeaders["Prismic-integration-field-ref"] = cachedRepository.integrationFieldsRef } // Normalize header keys to lowercase. This prevents header // conflicts between the Prismic client and the GraphQL // client. const headers: Record<string, string> = {} for (const key in unsanitizedHeaders) { if (unsanitizedHeaders[key]) { headers[key.toLowerCase()] = unsanitizedHeaders[key as keyof typeof unsanitizedHeaders] } } const url = new URL( // Asserting `input` is a string since popular GraphQL // libraries pass this as a string. Request objects as // input are unsupported. input as string, ) // This prevents the request from being cached unnecessarily. // Without adding this `ref` param, re-running a query // could return a locally cached response, even if the // `ref` changed. This happens because the URL is // identical when the `ref` is not included. Caches may ignore // headers. // // The Prismic GraphQL API ignores the `ref` param. url.searchParams.set("ref", ref) const query = url.searchParams.get("query") if (query) { url.searchParams.set( "query", // Compress the GraphQL query (if it exists) by // removing whitespace. This is done to // optimize the query size and avoid // hitting the upper limit of GET requests // (2048 characters). minifyGraphQLQuery(query), ) } return (await this.fetchFn(url.toString(), { ...init, headers, })) as Response } /** * Returns a cached version of `getRepository` with a TTL. * * @returns Cached repository metadata. */ private async getCachedRepository(params?: FetchParams): Promise<Repository> { if ( !this.cachedRepository || Date.now() >= this.cachedRepositoryExpiration ) { this.cachedRepository = await this.getRepository(params) this.cachedRepositoryExpiration = Date.now() + REPOSITORY_CACHE_TTL } return this.cachedRepository } /** * 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 if