Skip to content

Commit

Permalink
Merge branch 'master' of github.com:prismicio/prismic-client into aa/…
Browse files Browse the repository at this point in the history
…richtext
  • Loading branch information
lihbr committed Oct 5, 2023
2 parents 5df7215 + b43557d commit f166735
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 132 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
38 changes: 38 additions & 0 deletions src/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -120,10 +129,18 @@ export interface RequestInitLike extends Pick<RequestInit, "cache"> {
*/
export interface ResponseLike {
status: number;
headers: HeadersLike;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
json(): Promise<any>;
}

/**
* 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.
Expand Down Expand Up @@ -342,6 +359,7 @@ type ResolvePreviewArgs<LinkResolverReturnType> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FetchJobResult<TJSON = any> = {
status: number;
headers: HeadersLike;
json: TJSON;
};

Expand Down Expand Up @@ -1839,6 +1857,7 @@ export class Client<TDocuments extends PrismicDocument = PrismicDocument> {

return {
status: res.status,
headers: res.headers,
json,
};
})
Expand Down Expand Up @@ -1896,6 +1915,25 @@ export class Client<TDocuments extends PrismicDocument = PrismicDocument> {
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);
Expand Down
214 changes: 152 additions & 62 deletions src/helpers/asHTML.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Element,
RichTextFunctionSerializer,
RichTextMapSerializer,
RichTextMapSerializerFunction,
Expand Down Expand Up @@ -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<string>]: P extends RichTextMapSerializer<string>["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<string>]: (payload: {
type: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["type"];
node: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["node"];
Expand Down Expand Up @@ -105,6 +119,21 @@ type ExtractTextTypeGeneric<T> = 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
Expand All @@ -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<string> => {
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<string>,
>(
nodeSerializerOrShorthand: HTMLRichTextMapSerializer[BlockType],
defaultWithShorthand: NonNullable<
HTMLStrictRichTextMapSerializer[BlockType]
>,
): NonNullable<HTMLStrictRichTextMapSerializer[BlockType]> => {
if (typeof nodeSerializerOrShorthand === "function") {
return ((payload) => {
return (
(
nodeSerializerOrShorthand as HTMLStrictRichTextMapSerializer[BlockType]
)(payload) || defaultWithShorthand(payload)
);
}) as NonNullable<HTMLStrictRichTextMapSerializer[BlockType]>;
}

return defaultWithShorthand;
};

const mapSerializer: Required<HTMLStrictRichTextMapSerializer> = {
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);
};

/**
Expand All @@ -180,7 +265,7 @@ const createDefaultHTMLRichTextSerializer = (
* @returns A regular function serializer
*/
const wrapMapSerializerWithStringChildren = (
mapSerializer: HTMLRichTextMapSerializer,
mapSerializer: HTMLStrictRichTextMapSerializer,
): RichTextFunctionSerializer<string> => {
const modifiedMapSerializer = {} as RichTextMapSerializer<string>;

Expand Down Expand Up @@ -292,22 +377,27 @@ export const asHTML: {

let serializer: RichTextFunctionSerializer<string>;
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(
Expand Down
Loading

0 comments on commit f166735

Please sign in to comment.