Skip to content

Commit

Permalink
api-fetch: Type the rest of the package (#30161)
Browse files Browse the repository at this point in the history
* api-fetch: Type the rest of the package

* Update CHANGELOG

* Undo spread operator

* Fall back to window.location

* Use location.href instead of toString and fix comments
  • Loading branch information
sarayourfriend authored and gziolo committed Mar 26, 2021
1 parent 1757e5b commit 8958894
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 35 deletions.
2 changes: 2 additions & 0 deletions packages/api-fetch/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Publish TypeScript definitions.

## 3.22.0 (2021-03-17)

## 3.8.1 (2019-04-22)
Expand Down
71 changes: 54 additions & 17 deletions packages/api-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
* Default set of header values which should be sent with every request unless
* explicitly provided through apiFetch options.
*
* @type {Object}
* @type {Record<string, string>}
*/
const DEFAULT_HEADERS = {
// The backend uses the Accept header as a condition for considering an
Expand All @@ -43,17 +43,32 @@ const DEFAULT_OPTIONS = {
credentials: 'include',
};

/**
* @type {import('./types').ApiFetchMiddleware[]}
*/
const middlewares = [
userLocaleMiddleware,
namespaceEndpointMiddleware,
httpV1Middleware,
fetchAllMiddleware,
];

/**
* Register a middleware
*
* @param {import('./types').ApiFetchMiddleware} middleware
*/
function registerMiddleware( middleware ) {
middlewares.unshift( middleware );
}

/**
* Checks the status of a response, throwing the Response as an error if
* it is outside the 200 range.
*
* @param {Response} response
* @return {Response} The response if the status is in the 200 range.
*/
const checkStatus = ( response ) => {
if ( response.status >= 200 && response.status < 300 ) {
return response;
Expand All @@ -62,6 +77,11 @@ const checkStatus = ( response ) => {
throw response;
};

/** @typedef {(options: import('./types').ApiFetchRequestProps) => Promise<any>} FetchHandler*/

/**
* @type {FetchHandler}
*/
const defaultFetchHandler = ( nextOptions ) => {
const { url, path, data, parse = true, ...remainingOptions } = nextOptions;
let { body, headers } = nextOptions;
Expand All @@ -75,12 +95,16 @@ const defaultFetchHandler = ( nextOptions ) => {
headers[ 'Content-Type' ] = 'application/json';
}

const responsePromise = window.fetch( url || path, {
...DEFAULT_OPTIONS,
...remainingOptions,
body,
headers,
} );
const responsePromise = window.fetch(
// fall back to explicitly passing `window.location` which is the behavior if `undefined` is passed
url || path || window.location.href,
{
...DEFAULT_OPTIONS,
...remainingOptions,
body,
headers,
}
);

return (
responsePromise
Expand All @@ -107,25 +131,34 @@ const defaultFetchHandler = ( nextOptions ) => {
);
};

/** @type {FetchHandler} */
let fetchHandler = defaultFetchHandler;

/**
* Defines a custom fetch handler for making the requests that will override
* the default one using window.fetch
*
* @param {Function} newFetchHandler The new fetch handler
* @param {FetchHandler} newFetchHandler The new fetch handler
*/
function setFetchHandler( newFetchHandler ) {
fetchHandler = newFetchHandler;
}

/**
* @template T
* @param {import('./types').ApiFetchRequestProps} options
* @return {Promise<T>} A promise representing the request processed via the registered middlewares.
*/
function apiFetch( options ) {
// creates a nested function chain that calls all middlewares and finally the `fetchHandler`,
// converting `middlewares = [ m1, m2, m3 ]` into:
// ```
// opts1 => m1( opts1, opts2 => m2( opts2, opts3 => m3( opts3, fetchHandler ) ) );
// ```
const enhancedHandler = middlewares.reduceRight( ( next, middleware ) => {
const enhancedHandler = middlewares.reduceRight( (
/** @type {FetchHandler} */ next,
middleware
) => {
return ( workingOptions ) => middleware( workingOptions, next );
}, fetchHandler );

Expand All @@ -135,14 +168,18 @@ function apiFetch( options ) {
}

// If the nonce is invalid, refresh it and try again.
return window
.fetch( apiFetch.nonceEndpoint )
.then( checkStatus )
.then( ( data ) => data.text() )
.then( ( text ) => {
apiFetch.nonceMiddleware.nonce = text;
return apiFetch( options );
} );
return (
window
// @ts-ignore
.fetch( apiFetch.nonceEndpoint )
.then( checkStatus )
.then( ( data ) => data.text() )
.then( ( text ) => {
// @ts-ignore
apiFetch.nonceMiddleware.nonce = text;
return apiFetch( options );
} )
);
} );
}

Expand Down
43 changes: 35 additions & 8 deletions packages/api-fetch/src/middlewares/fetch-all-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,32 @@ import { addQueryArgs } from '@wordpress/url';
*/
import apiFetch from '..';

// Apply query arguments to both URL and Path, whichever is present.
/**
* Apply query arguments to both URL and Path, whichever is present.
*
* @param {import('../types').ApiFetchRequestProps} props
* @param {Record<string, string | number>} queryArgs
* @return {import('../types').ApiFetchRequestProps} The request with the modified query args
*/
const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( {
...options,
url: url && addQueryArgs( url, queryArgs ),
path: path && addQueryArgs( path, queryArgs ),
} );

// Duplicates parsing functionality from apiFetch.
/**
* Duplicates parsing functionality from apiFetch.
*
* @param {Response} response
* @return {Promise<any>} Parsed response json.
*/
const parseResponse = ( response ) =>
response.json ? response.json() : Promise.reject( response );

/**
* @param {string | null} linkHeader
* @return {{ next?: string }} The parsed link header.
*/
const parseLinkHeader = ( linkHeader ) => {
if ( ! linkHeader ) {
return {};
Expand All @@ -31,22 +46,34 @@ const parseLinkHeader = ( linkHeader ) => {
: {};
};

/**
* @param {Response} response
* @return {string | undefined} The next page URL.
*/
const getNextPageUrl = ( response ) => {
const { next } = parseLinkHeader( response.headers.get( 'link' ) );
return next;
};

/**
* @param {import('../types').ApiFetchRequestProps} options
* @return {boolean} True if the request contains an unbounded query.
*/
const requestContainsUnboundedQuery = ( options ) => {
const pathIsUnbounded =
options.path && options.path.indexOf( 'per_page=-1' ) !== -1;
!! options.path && options.path.indexOf( 'per_page=-1' ) !== -1;
const urlIsUnbounded =
options.url && options.url.indexOf( 'per_page=-1' ) !== -1;
!! options.url && options.url.indexOf( 'per_page=-1' ) !== -1;
return pathIsUnbounded || urlIsUnbounded;
};

// The REST API enforces an upper limit on the per_page option. To handle large
// collections, apiFetch consumers can pass `per_page=-1`; this middleware will
// then recursively assemble a full response array from all available pages.
/**
* The REST API enforces an upper limit on the per_page option. To handle large
* collections, apiFetch consumers can pass `per_page=-1`; this middleware will
* then recursively assemble a full response array from all available pages.
*
* @type {import('../types').ApiFetchMiddleware}
*/
const fetchAllMiddleware = async ( options, next ) => {
if ( options.parse === false ) {
// If a consumer has opted out of parsing, do not apply middleware.
Expand Down Expand Up @@ -81,7 +108,7 @@ const fetchAllMiddleware = async ( options, next ) => {
}

// Iteratively fetch all remaining pages until no "next" header is found.
let mergedResults = [].concat( results );
let mergedResults = /** @type {any[]} */ ( [] ).concat( results );
while ( nextPage ) {
const nextResponse = await apiFetch( {
...options,
Expand Down
2 changes: 1 addition & 1 deletion packages/api-fetch/src/middlewares/nonce.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @param {string} nonce
* @return {import('../types').ApiFetchMiddleware} A middleware to enhance a request with a nonce.
* @return {import('../types').ApiFetchMiddleware & { nonce: string }} A middleware to enhance a request with a nonce.
*/
function createNonceMiddleware( nonce ) {
/**
Expand Down
14 changes: 5 additions & 9 deletions packages/api-fetch/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@
"declarationDir": "build-types"
},
"references": [
{ "path": "../i18n" },
{ "path": "../url" }
],
"include": [
"src/middlewares/http-v1.js",
"src/middlewares/media-upload.js",
"src/middlewares/namespace-endpoint.js",
"src/middlewares/nonce.js",
"src/middlewares/preloading.js",
"src/middlewares/root-url.js",
"src/middlewares/user-locale.js",
"src/utils/**/*",
"src/types.ts"
"src/**/*",
],
"exclude": [
"**/test/**/*",
]
}

0 comments on commit 8958894

Please sign in to comment.