diff --git a/CHANGELOG.md b/CHANGELOG.md index a109d10b..ab5a8577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.2.0](https://github.com/prismicio/prismic-client/compare/v7.1.1...v7.2.0) (2023-08-25) + + +### Features + +* automatically retry rate-limited requests ([#319](https://github.com/prismicio/prismic-client/issues/319)) ([e0c8c49](https://github.com/prismicio/prismic-client/commit/e0c8c49fafac235c070550efbd45dc9dcbda4027)) + ### [7.1.1](https://github.com/prismicio/prismic-client/compare/v7.1.0...v7.1.1) (2023-08-11) diff --git a/package-lock.json b/package-lock.json index dc0d4b7e..fdf8c517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@prismicio/client", - "version": "7.1.1", + "version": "7.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@prismicio/client", - "version": "7.1.1", + "version": "7.2.0", "license": "Apache-2.0", "dependencies": { "@prismicio/richtext": "^2.1.5", diff --git a/package.json b/package.json index 7a9f9824..986cd3bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@prismicio/client", - "version": "7.1.1", + "version": "7.2.0", "description": "The official JavaScript + TypeScript client library for Prismic", "keywords": [ "typescript", diff --git a/src/createClient.ts b/src/createClient.ts index 88e75eea..1929410d 100644 --- a/src/createClient.ts +++ b/src/createClient.ts @@ -51,6 +51,15 @@ export const REPOSITORY_CACHE_TTL = 5000; */ export const GET_ALL_QUERY_DELAY = 500; +/** + * The default number of milliseconds to wait before retrying a rate-limited + * `fetch()` request (429 response code). The default value is only used if the + * response does not include a `retry-after` header. + * + * The API allows up to 200 requests per second. + */ +const DEFUALT_RETRY_AFTER_MS = 1000; + /** * 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 @@ -120,10 +129,18 @@ export interface RequestInitLike extends Pick { */ export interface ResponseLike { status: number; + headers: HeadersLike; // eslint-disable-next-line @typescript-eslint/no-explicit-any json(): Promise; } +/** + * The minimum required properties from Headers. + */ +export interface HeadersLike { + get(name: string): string | null; +} + /** * The minimum required properties to treat as an HTTP Request for automatic * Prismic preview support. @@ -342,6 +359,7 @@ type ResolvePreviewArgs = { // eslint-disable-next-line @typescript-eslint/no-explicit-any type FetchJobResult = { status: number; + headers: HeadersLike; json: TJSON; }; @@ -1839,6 +1857,7 @@ export class Client { return { status: res.status, + headers: res.headers, json, }; }) @@ -1896,6 +1915,25 @@ export class Client { undefined, ); } + + // Too Many Requests + // - Exceeded the maximum number of requests per second + case 429: { + const parsedRetryAfter = Number(res.headers.get("retry-after")); + const delay = Number.isNaN(parsedRetryAfter) + ? DEFUALT_RETRY_AFTER_MS + : parsedRetryAfter; + + return await new Promise((resolve, reject) => { + setTimeout(async () => { + try { + resolve(await this.fetch(url, params)); + } catch (error) { + reject(error); + } + }, delay); + }); + } } throw new PrismicError(undefined, url, res.json); diff --git a/src/helpers/asHTML.ts b/src/helpers/asHTML.ts index 27db4aff..fef4b1c6 100644 --- a/src/helpers/asHTML.ts +++ b/src/helpers/asHTML.ts @@ -1,5 +1,4 @@ import { - Element, RichTextFunctionSerializer, RichTextMapSerializer, RichTextMapSerializerFunction, @@ -43,11 +42,26 @@ export type HTMLRichTextFunctionSerializer = ( * * Unlike a typical `@prismicio/richtext` map serializer, this serializer * converts the `children` property to a single string rather than an array of - * strings. + * strings and accepts shorthand declarations. * * @see Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title} */ export type HTMLRichTextMapSerializer = { + [P in keyof RichTextMapSerializer]: P extends RichTextMapSerializer["span"] + ? HTMLStrictRichTextMapSerializer[P] + : HTMLStrictRichTextMapSerializer[P] | HTMLRichTextMapSerializerShorthand; +}; + +/** + * Serializes a node from a rich text or title field with a map to HTML + * + * Unlike a typical `@prismicio/richtext` map serializer, this serializer + * converts the `children` property to a single string rather than an array of + * strings but doesn't accept shorthand declarations. + * + * @see Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title} + */ +export type HTMLStrictRichTextMapSerializer = { [P in keyof RichTextMapSerializer]: (payload: { type: Parameters>[0]["type"]; node: Parameters>[0]["node"]; @@ -105,6 +119,21 @@ type ExtractTextTypeGeneric = T extends RichTextMapSerializerFunction< ? U : never; +/** + * A shorthand definition for {@link HTMLRichTextMapSerializer} element types. + */ +export type HTMLRichTextMapSerializerShorthand = { + /** + * Classes to apply to the element type. + */ + class?: string; + + /** + * Other attributes to apply to the element type. + */ + [Attribute: string]: string | boolean | null | undefined; +}; + /** * Serializes a node from a rich text or title field with a map or a function to * HTML @@ -117,57 +146,113 @@ export type HTMLRichTextSerializer = | HTMLRichTextFunctionSerializer; /** - * Creates a default HTML rich text serializer with a given link resolver - * providing sensible and safe defaults for every node type + * Creates a HTML rich text serializer with a given link resolver and provide + * sensible and safe defaults for every node type * * @internal */ -const createDefaultHTMLRichTextSerializer = ( +const createHTMLRichTextSerializer = ( linkResolver: LinkResolverFunction | undefined | null, + serializer?: HTMLRichTextMapSerializer | null, ): RichTextFunctionSerializer => { - return (_type, node, text, children, _key) => { - switch (node.type) { - case Element.heading1: - return serializeStandardTag("h1", node, children); - case Element.heading2: - return serializeStandardTag("h2", node, children); - case Element.heading3: - return serializeStandardTag("h3", node, children); - case Element.heading4: - return serializeStandardTag("h4", node, children); - case Element.heading5: - return serializeStandardTag("h5", node, children); - case Element.heading6: - return serializeStandardTag("h6", node, children); - case Element.paragraph: - return serializeStandardTag("p", node, children); - case Element.preformatted: - return serializePreFormatted(node); - case Element.strong: - return serializeStandardTag("strong", node, children); - case Element.em: - return serializeStandardTag("em", node, children); - case Element.listItem: - return serializeStandardTag("li", node, children); - case Element.oListItem: - return serializeStandardTag("li", node, children); - case Element.list: - return serializeStandardTag("ul", node, children); - case Element.oList: - return serializeStandardTag("ol", node, children); - case Element.image: - return serializeImage(linkResolver, node); - case Element.embed: - return serializeEmbed(node); - case Element.hyperlink: - return serializeHyperlink(linkResolver, node, children); - case Element.label: - return serializeStandardTag("span", node, children); - case Element.span: - default: - return serializeSpan(text); + const useSerializerOrDefault = < + BlockType extends keyof RichTextMapSerializer, + >( + nodeSerializerOrShorthand: HTMLRichTextMapSerializer[BlockType], + defaultWithShorthand: NonNullable< + HTMLStrictRichTextMapSerializer[BlockType] + >, + ): NonNullable => { + if (typeof nodeSerializerOrShorthand === "function") { + return ((payload) => { + return ( + ( + nodeSerializerOrShorthand as HTMLStrictRichTextMapSerializer[BlockType] + )(payload) || defaultWithShorthand(payload) + ); + }) as NonNullable; } + + return defaultWithShorthand; }; + + const mapSerializer: Required = { + heading1: useSerializerOrDefault<"heading1">( + serializer?.heading1, + serializeStandardTag<"heading1">("h1", serializer?.heading1), + ), + heading2: useSerializerOrDefault<"heading2">( + serializer?.heading2, + serializeStandardTag<"heading2">("h2", serializer?.heading2), + ), + heading3: useSerializerOrDefault<"heading3">( + serializer?.heading3, + serializeStandardTag<"heading3">("h3", serializer?.heading3), + ), + heading4: useSerializerOrDefault<"heading4">( + serializer?.heading4, + serializeStandardTag<"heading4">("h4", serializer?.heading4), + ), + heading5: useSerializerOrDefault<"heading5">( + serializer?.heading5, + serializeStandardTag<"heading5">("h5", serializer?.heading5), + ), + heading6: useSerializerOrDefault<"heading6">( + serializer?.heading6, + serializeStandardTag<"heading6">("h6", serializer?.heading6), + ), + paragraph: useSerializerOrDefault<"paragraph">( + serializer?.paragraph, + serializeStandardTag<"paragraph">("p", serializer?.paragraph), + ), + preformatted: useSerializerOrDefault<"preformatted">( + serializer?.preformatted, + serializePreFormatted(serializer?.preformatted), + ), + strong: useSerializerOrDefault<"strong">( + serializer?.strong, + serializeStandardTag<"strong">("strong", serializer?.strong), + ), + em: useSerializerOrDefault<"em">( + serializer?.em, + serializeStandardTag<"em">("em", serializer?.em), + ), + listItem: useSerializerOrDefault<"listItem">( + serializer?.listItem, + serializeStandardTag<"listItem">("li", serializer?.listItem), + ), + oListItem: useSerializerOrDefault<"oListItem">( + serializer?.oListItem, + serializeStandardTag<"oListItem">("li", serializer?.oListItem), + ), + list: useSerializerOrDefault<"list">( + serializer?.list, + serializeStandardTag<"list">("ul", serializer?.list), + ), + oList: useSerializerOrDefault<"oList">( + serializer?.oList, + serializeStandardTag<"oList">("ol", serializer?.oList), + ), + image: useSerializerOrDefault<"image">( + serializer?.image, + serializeImage(linkResolver, serializer?.image), + ), + embed: useSerializerOrDefault<"embed">( + serializer?.embed, + serializeEmbed(serializer?.embed), + ), + hyperlink: useSerializerOrDefault<"hyperlink">( + serializer?.hyperlink, + serializeHyperlink(linkResolver, serializer?.hyperlink), + ), + label: useSerializerOrDefault<"label">( + serializer?.label, + serializeStandardTag<"label">("span", serializer?.label), + ), + span: useSerializerOrDefault<"span">(serializer?.span, serializeSpan()), + }; + + return wrapMapSerializerWithStringChildren(mapSerializer); }; /** @@ -180,7 +265,7 @@ const createDefaultHTMLRichTextSerializer = ( * @returns A regular function serializer */ const wrapMapSerializerWithStringChildren = ( - mapSerializer: HTMLRichTextMapSerializer, + mapSerializer: HTMLStrictRichTextMapSerializer, ): RichTextFunctionSerializer => { const modifiedMapSerializer = {} as RichTextMapSerializer; @@ -292,22 +377,27 @@ export const asHTML: { let serializer: RichTextFunctionSerializer; if (config.serializer) { - serializer = composeSerializers( - typeof config.serializer === "object" - ? wrapMapSerializerWithStringChildren(config.serializer) - : (type, node, text, children, key) => - // TypeScript doesn't narrow the type correctly here since it is now in a callback function, so we have to cast it here. - (config.serializer as HTMLRichTextFunctionSerializer)( - type, - node, - text, - children.join(""), - key, - ), - createDefaultHTMLRichTextSerializer(config.linkResolver), - ); + if (typeof config.serializer === "function") { + serializer = composeSerializers( + (type, node, text, children, key) => + // TypeScript doesn't narrow the type correctly here since it is now in a callback function, so we have to cast it here. + (config.serializer as HTMLRichTextFunctionSerializer)( + type, + node, + text, + children.join(""), + key, + ), + createHTMLRichTextSerializer(config.linkResolver), + ); + } else { + serializer = createHTMLRichTextSerializer( + config.linkResolver, + config.serializer, + ); + } } else { - serializer = createDefaultHTMLRichTextSerializer(config.linkResolver); + serializer = createHTMLRichTextSerializer(config.linkResolver); } return serialize(richTextField, serializer).join( diff --git a/src/lib/serializerHelpers.ts b/src/lib/serializerHelpers.ts index 028d4ef9..c1279d7d 100644 --- a/src/lib/serializerHelpers.ts +++ b/src/lib/serializerHelpers.ts @@ -1,34 +1,83 @@ +import { RichTextMapSerializer } from "@prismicio/richtext"; + import { LinkType } from "../types/value/link"; -import { - RTBlockNode, - RTEmbedNode, - RTImageNode, - RTInlineNode, - RTLinkNode, - RTPreformattedNode, - RichTextNodeType, -} from "../types/value/richText"; +import { RTAnyNode } from "../types/value/richText"; +import { + HTMLRichTextMapSerializer, + HTMLStrictRichTextMapSerializer, +} from "../helpers/asHTML"; import { LinkResolverFunction, asLink } from "../helpers/asLink"; import { escapeHTML } from "./escapeHTML"; -export const getLabel = (node: RTBlockNode | RTInlineNode): string => { - return "data" in node && "label" in node.data - ? ` class="${node.data.label}"` - : ""; +type Attributes = Record; +const formatAttributes = (node: RTAnyNode, attributes: Attributes): string => { + const _attributes = { ...attributes }; + + // Add label to attributes + if ("data" in node && "label" in node.data && node.data.label) { + _attributes.class = _attributes.class + ? `${_attributes.class} ${node.data.label}` + : node.data.label; + } + + const result = []; + + for (const key in _attributes) { + const value = _attributes[key]; + + if (value) { + if (typeof value === "boolean") { + result.push(key); + } else { + result.push(`${key}="${escapeHTML(value)}"`); + } + } + } + + // Add a space at the beginning if there's any result + if (result.length) { + result.unshift(""); + } + + return result.join(" "); +}; + +const getGeneralAttributes = ( + serializerOrShorthand?: HTMLRichTextMapSerializer[keyof HTMLRichTextMapSerializer], +): Attributes => { + return serializerOrShorthand && typeof serializerOrShorthand !== "function" + ? serializerOrShorthand + : {}; }; -export const serializeStandardTag = ( +export const serializeStandardTag = < + BlockType extends keyof RichTextMapSerializer, +>( tag: string, - node: RTBlockNode | RTInlineNode, - children: string[], -): string => { - return `<${tag}${getLabel(node)}>${children.join("")}`; + serializerOrShorthand?: HTMLRichTextMapSerializer[BlockType], +): NonNullable => { + const generalAttributes = getGeneralAttributes(serializerOrShorthand); + + return (({ node, children }) => { + return `<${tag}${formatAttributes( + node, + generalAttributes, + )}>${children}`; + }) as NonNullable; }; -export const serializePreFormatted = (node: RTPreformattedNode): string => { - return `${escapeHTML(node.text)}`; +export const serializePreFormatted = ( + serializerOrShorthand?: HTMLRichTextMapSerializer["preformatted"], +): NonNullable => { + const generalAttributes = getGeneralAttributes(serializerOrShorthand); + + return ({ node }) => { + return `${escapeHTML( + node.text, + )}`; + }; }; export const serializeImage = ( @@ -36,35 +85,57 @@ export const serializeImage = ( | LinkResolverFunction | undefined | null, - node: RTImageNode, -): string => { - let imageTag = `${escapeHTML(node.alt)}`; - - // If the image has a link, we wrap it with an anchor tag - if (node.linkTo) { - imageTag = serializeHyperlink( - linkResolver, - { - type: RichTextNodeType.hyperlink, - data: node.linkTo, - start: 0, - end: 0, - }, - [imageTag], - ); - } + serializerOrShorthand?: HTMLRichTextMapSerializer["image"], +): NonNullable => { + const generalAttributes = getGeneralAttributes(serializerOrShorthand); + + return ({ node }) => { + const attributes = { + ...generalAttributes, + src: node.url, + alt: node.alt, + copyright: node.copyright, + }; + + let imageTag = ``; + + // If the image has a link, we wrap it with an anchor tag + if (node.linkTo) { + imageTag = serializeHyperlink(linkResolver)({ + type: "hyperlink", + node: { + type: "hyperlink", + data: node.linkTo, + start: 0, + end: 0, + }, + text: "", + children: imageTag, + key: "", + })!; + } - return `

${imageTag}

`; + return `

${imageTag}

`; + }; }; -export const serializeEmbed = (node: RTEmbedNode): string => { - return `
${ - node.oembed.html - }
`; +export const serializeEmbed = ( + serializerOrShorthand?: HTMLRichTextMapSerializer["embed"], +): NonNullable => { + const generalAttributes = getGeneralAttributes(serializerOrShorthand); + + return ({ node }) => { + const attributes = { + ...generalAttributes, + ["data-oembed"]: node.oembed.embed_url, + ["data-oembed-type"]: node.oembed.type, + ["data-oembed-provider"]: node.oembed.provider_name, + }; + + return `${ + node.oembed.html + }`; + }; }; export const serializeHyperlink = ( @@ -72,30 +143,33 @@ export const serializeHyperlink = ( | LinkResolverFunction | undefined | null, - node: RTLinkNode, - children: string[], -): string => { - switch (node.data.link_type) { - case LinkType.Web: { - return `${children.join("")}`; - } + serializerOrShorthand?: HTMLRichTextMapSerializer["hyperlink"], +): NonNullable => { + const generalAttributes = getGeneralAttributes(serializerOrShorthand); - case LinkType.Document: { - return `${children.join("")}`; - } + return ({ node, children }): string => { + const attributes = { + ...generalAttributes, + }; - case LinkType.Media: { - return `${children.join( - "", - )}`; + if (node.data.link_type === LinkType.Web) { + attributes.href = node.data.url; + attributes.target = node.data.target; + attributes.rel = "noopener noreferrer"; + } else if (node.data.link_type === LinkType.Document) { + attributes.href = asLink(node.data, { linkResolver }); + } else if (node.data.link_type === LinkType.Media) { + attributes.href = node.data.url; } - } + + return `${children}`; + }; }; -export const serializeSpan = (content?: string): string => { - return content ? escapeHTML(content).replace(/\n/g, "
") : ""; +export const serializeSpan = (): NonNullable< + HTMLStrictRichTextMapSerializer["span"] +> => { + return ({ text }): string => { + return text ? escapeHTML(text).replace(/\n/g, "
") : ""; + }; }; diff --git a/test/__fixtures__/partialHTMLRichTextMapSerializer.ts b/test/__fixtures__/partialHTMLRichTextMapSerializer.ts index fdfe4cef..3060ee55 100644 --- a/test/__fixtures__/partialHTMLRichTextMapSerializer.ts +++ b/test/__fixtures__/partialHTMLRichTextMapSerializer.ts @@ -5,4 +5,6 @@ export const partialHTMLRichTextMapSerializer: prismic.HTMLRichTextMapSerializer heading1: ({ children }) => `

${children}

`, // `undefined` serializers should be treated the same as not including it. heading2: undefined, + // `undefined` returning serializers should fallback to default serializer. + heading3: () => undefined, }; diff --git a/test/__snapshots__/helpers-asHTML.test.ts.snap b/test/__snapshots__/helpers-asHTML.test.ts.snap index 3e779583..4463f388 100644 --- a/test/__snapshots__/helpers-asHTML.test.ts.snap +++ b/test/__snapshots__/helpers-asHTML.test.ts.snap @@ -6,4 +6,6 @@ exports[`serializes with a custom function serializer 1`] = `"

Lorem ipsum do exports[`serializes with a custom map serializer 1`] = `"

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  2. Lorem ipsum dolor sit amet.
  3. Lorem ipsum dolor sit amet.
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  • Lorem ipsum dolor sit amet.
  • Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

\\"An

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

"`; +exports[`serializes with a custom shorthand map serializer 1`] = `"

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  2. Lorem ipsum dolor sit amet.
  3. Lorem ipsum dolor sit amet.
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  • Lorem ipsum dolor sit amet.
  • Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

\\"An

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

"`; + exports[`serializes with default serializer 1`] = `"

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

  1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  2. Lorem ipsum dolor sit amet.
  3. Lorem ipsum dolor sit amet.
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  • Lorem ipsum dolor sit amet.
  • Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

\\"An

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\"An

"`; diff --git a/test/client.test.ts b/test/client.test.ts index b54bd1b0..c4bcbe9a 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -683,3 +683,173 @@ it("throws NotFoundError if repository does not exist", async (ctx) => { ); await expect(() => client.get()).rejects.toThrowError(prismic.NotFoundError); }); + +it("retries after `retry-after` milliseconds if response code is 429", async (ctx) => { + const retryAfter = 200; // ms + /** + * The number of milliseconds that time-measuring tests can vary. + */ + const testTolerance = 100; + /** + * The number of times 429 is returned. + */ + const retryResponseQty = 2; + + const queryResponse = prismicM.api.query({ seed: ctx.task.name }); + + mockPrismicRestAPIV2({ + ctx, + queryResponse, + }); + + const client = createTestClient(); + + const queryEndpoint = new URL( + "documents/search", + `${client.endpoint}/`, + ).toString(); + + let responseTries = 0; + + // Override the query endpoint to return a 429 while `responseTries` is + // less than or equal to `retryResponseQty` + ctx.server.use( + msw.rest.get(queryEndpoint, (_req, res, ctx) => { + responseTries++; + + if (responseTries <= retryResponseQty) { + return res( + ctx.status(429), + ctx.json({ + status_code: 429, + status_message: + "Your request count (11) is over the allowed limit of 10.", + }), + ctx.set("retry-after", retryAfter.toString()), + ); + } + }), + ); + + // Rate limited. Should resolve roughly after retryAfter * retryResponseQty milliseconds. + const t0_0 = performance.now(); + const res0 = await client.get(); + const t0_1 = performance.now(); + + expect(res0).toStrictEqual(queryResponse); + expect(t0_1 - t0_0).toBeGreaterThanOrEqual(retryAfter * retryResponseQty); + expect(t0_1 - t0_0).toBeLessThanOrEqual( + retryAfter * retryResponseQty + testTolerance, + ); + + // Not rate limited. Should resolve nearly immediately. + const t1_0 = performance.now(); + const res1 = await client.get(); + const t1_1 = performance.now(); + + expect(res1).toStrictEqual(queryResponse); + expect(t1_1 - t1_0).toBeGreaterThanOrEqual(0); + expect(t1_1 - t1_0).toBeLessThanOrEqual(testTolerance); +}); + +it("retries after 1000 milliseconds if response code is 429 and an invalid `retry-after` value is returned", async (ctx) => { + /** + * The number of milliseconds that time-measuring tests can vary. + */ + const testTolerance = 100; + + const queryResponse = prismicM.api.query({ seed: ctx.task.name }); + + mockPrismicRestAPIV2({ + ctx, + queryResponse, + }); + + const client = createTestClient(); + + const queryEndpoint = new URL( + "documents/search", + `${client.endpoint}/`, + ).toString(); + + let responseTries = 0; + + // Override the query endpoint to return a 429 while `responseTries` is + // less than or equal to `retryResponseQty` + ctx.server.use( + msw.rest.get(queryEndpoint, (_req, res, ctx) => { + responseTries++; + + if (responseTries <= 1) { + return res( + ctx.status(429), + ctx.json({ + status_code: 429, + status_message: + "Your request count (11) is over the allowed limit of 10.", + }), + ctx.set("retry-after", "invalid"), + ); + } + }), + ); + + // Rate limited. Should resolve roughly after 1000 milliseconds. + const t0 = performance.now(); + const res = await client.get(); + const t1 = performance.now(); + + expect(res).toStrictEqual(queryResponse); + expect(t1 - t0).toBeGreaterThanOrEqual(1000); + expect(t1 - t0).toBeLessThanOrEqual(1000 + testTolerance); +}); + +it("throws if a non-2xx response is returned even after retrying", async (ctx) => { + /** + * The number of milliseconds that time-measuring tests can vary. + */ + const testTolerance = 100; + + mockPrismicRestAPIV2({ ctx }); + + const client = createTestClient(); + + const queryEndpoint = new URL( + "documents/search", + `${client.endpoint}/`, + ).toString(); + + let responseTries = 0; + + // Override the query endpoint to return a 429 while `responseTries` is + // less than or equal to `retryResponseQty` + ctx.server.use( + msw.rest.get(queryEndpoint, (_req, res, ctx) => { + responseTries++; + + if (responseTries <= 1) { + return res( + ctx.status(429), + ctx.json({ + status_code: 429, + status_message: + "Your request count (11) is over the allowed limit of 10.", + }), + ctx.set("retry-after", "invalid"), + ); + } else { + return res(ctx.status(418)); + } + }), + ); + + // Rate limited. Should reject roughly after 1000 milliseconds. + const t0 = performance.now(); + await expect(() => client.get()).rejects.toThrowError( + /invalid api response/i, + ); + const t1 = performance.now(); + + expect(t1 - t0).toBeGreaterThanOrEqual(1000); + expect(t1 - t0).toBeLessThanOrEqual(1000 + testTolerance); +}); diff --git a/test/helpers-asHTML.test.ts b/test/helpers-asHTML.test.ts index fb7e73f3..dd9c8696 100644 --- a/test/helpers-asHTML.test.ts +++ b/test/helpers-asHTML.test.ts @@ -65,6 +65,23 @@ it("serializes with a custom map serializer", () => { ); }); +it("serializes with a custom shorthand map serializer", () => { + const richTextFixtures = createRichTextFixtures(); + + expect( + asHTML(richTextFixtures.en, { + linkResolver, + serializer: { + heading1: { class: "text-xl", "data-heading": true }, + heading2: { + xss: 'https://example.org" onmouseover="alert(document.cookie);', + }, + label: { class: "shorthand" }, + }, + }), + ).toMatchSnapshot(); +}); + it("escapes external links to prevent XSS", () => { const richTextFixtures = createRichTextFixtures();