Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import {
parseResponseAndNormalizeError,
parseAndThrowError,
} from './utils/response';
import type {
APIFetchMiddleware,
APIFetchOptions,
FetchHandler,
} from './types';

/**
* Default set of header values which should be sent with every request unless
* explicitly provided through apiFetch options.
*
* @type {Record<string, string>}
*/
const DEFAULT_HEADERS = {
const DEFAULT_HEADERS: APIFetchOptions[ 'headers' ] = {
// The backend uses the Accept header as a condition for considering an
// incoming request as a REST request.
//
Expand All @@ -37,20 +40,12 @@ const DEFAULT_HEADERS = {
/**
* Default set of fetch option values which should be sent with every request
* unless explicitly provided through apiFetch options.
*
* @type {Object}
*/
const DEFAULT_OPTIONS = {
const DEFAULT_OPTIONS: APIFetchOptions = {
credentials: 'include',
};

/** @typedef {import('./types').APIFetchMiddleware} APIFetchMiddleware */
/** @typedef {import('./types').APIFetchOptions} APIFetchOptions */

/**
* @type {import('./types').APIFetchMiddleware[]}
*/
const middlewares = [
const middlewares: Array< APIFetchMiddleware > = [
userLocaleMiddleware,
namespaceEndpointMiddleware,
httpV1Middleware,
Expand All @@ -60,33 +55,28 @@ const middlewares = [
/**
* Register a middleware
*
* @param {import('./types').APIFetchMiddleware} middleware
* @param middleware
*/
function registerMiddleware( middleware ) {
function registerMiddleware( middleware: APIFetchMiddleware ) {
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.
* @param response
* @return The response if the status is in the 200 range.
*/
const checkStatus = ( response ) => {
const checkStatus = ( response: Response ) => {
if ( response.status >= 200 && response.status < 300 ) {
return response;
}

throw response;
};

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

/**
* @type {FetchHandler}
*/
const defaultFetchHandler = ( nextOptions ) => {
const defaultFetchHandler: FetchHandler = ( nextOptions ) => {
const { url, path, data, parse = true, ...remainingOptions } = nextOptions;
let { body, headers } = nextOptions;

Expand Down Expand Up @@ -134,32 +124,48 @@ 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 {FetchHandler} newFetchHandler The new fetch handler
* @param newFetchHandler The new fetch handler
*/
function setFetchHandler( newFetchHandler ) {
function setFetchHandler( newFetchHandler: FetchHandler ) {
fetchHandler = newFetchHandler;
}

interface apiFetch {
< T, Parse extends boolean = true >(
options: APIFetchOptions< Parse >
): Promise< Parse extends true ? T : Response >;
nonceEndpoint?: string;
nonceMiddleware?: ReturnType< typeof createNonceMiddleware >;
use: ( middleware: APIFetchMiddleware ) => void;
setFetchHandler: ( newFetchHandler: FetchHandler ) => void;
createNonceMiddleware: typeof createNonceMiddleware;
createPreloadingMiddleware: typeof createPreloadingMiddleware;
createRootURLMiddleware: typeof createRootURLMiddleware;
fetchAllMiddleware: typeof fetchAllMiddleware;
mediaUploadMiddleware: typeof mediaUploadMiddleware;
createThemePreviewMiddleware: typeof createThemePreviewMiddleware;
}

/**
* @template T
* @param {import('./types').APIFetchOptions} options
* @return {Promise<T>} A promise representing the request processed via the registered middlewares.
* Fetch
*
* @param options The options for the fetch.
* @return A promise representing the request processed via the registered middlewares.
*/
function apiFetch( options ) {
const apiFetch: 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(
( /** @type {FetchHandler} */ next, middleware ) => {
const enhancedHandler = middlewares.reduceRight< FetchHandler >(
( next, middleware ) => {
return ( workingOptions ) => middleware( workingOptions, next );
},
fetchHandler
Expand All @@ -171,20 +177,16 @@ function apiFetch( options ) {
}

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

apiFetch.use = registerMiddleware;
apiFetch.setFetchHandler = setFetchHandler;
Expand All @@ -197,3 +199,4 @@ apiFetch.mediaUploadMiddleware = mediaUploadMiddleware;
apiFetch.createThemePreviewMiddleware = createThemePreviewMiddleware;

export default apiFetch;
export * from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import { addQueryArgs } from '@wordpress/url';
* Internal dependencies
*/
import apiFetch from '..';
import type { APIFetchMiddleware, APIFetchOptions } from '../types';

/**
* Apply query arguments to both URL and Path, whichever is present.
*
* @param {import('../types').APIFetchOptions} props
* @param {Record<string, string | number>} queryArgs
* @return {import('../types').APIFetchOptions} The request with the modified query args
* @param {APIFetchOptions} props The request options
* @param {Record< string, string | number >} queryArgs
* @return The request with the modified query args
*/
const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( {
const modifyQuery = (
{ path, url, ...options }: APIFetchOptions,
queryArgs: Record< string, string | number >
): APIFetchOptions => ( {
...options,
url: url && addQueryArgs( url, queryArgs ),
path: path && addQueryArgs( path, queryArgs ),
Expand All @@ -24,17 +28,17 @@ const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( {
/**
* Duplicates parsing functionality from apiFetch.
*
* @param {Response} response
* @return {Promise<any>} Parsed response json.
* @param response
* @return Parsed response json.
*/
const parseResponse = ( response ) =>
const parseResponse = ( response: Response ) =>
response.json ? response.json() : Promise.reject( response );

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

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

/**
* @param {import('../types').APIFetchOptions} options
* @return {boolean} True if the request contains an unbounded query.
* @param options
* @return True if the request contains an unbounded query.
*/
const requestContainsUnboundedQuery = ( options ) => {
const requestContainsUnboundedQuery = ( options: APIFetchOptions ) => {
const pathIsUnbounded =
!! options.path && options.path.indexOf( 'per_page=-1' ) !== -1;
const urlIsUnbounded =
Expand All @@ -71,10 +75,10 @@ const requestContainsUnboundedQuery = ( options ) => {
* 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}
* @param options
* @param next
*/
const fetchAllMiddleware = async ( options, next ) => {
const fetchAllMiddleware: APIFetchMiddleware = async ( options, next ) => {
if ( options.parse === false ) {
// If a consumer has opted out of parsing, do not apply middleware.
return next( options );
Expand Down Expand Up @@ -108,7 +112,7 @@ const fetchAllMiddleware = async ( options, next ) => {
}

// Iteratively fetch all remaining pages until no "next" header is found.
let mergedResults = /** @type {any[]} */ ( [] ).concat( results );
let mergedResults = ( [] as Array< any > ).concat( results );
while ( nextPage ) {
const nextResponse = await apiFetch( {
...options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/**
* Internal dependencies
*/
import type { APIFetchMiddleware } from '../types';

/**
* Set of HTTP methods which are eligible to be overridden.
*
* @type {Set<string>}
*/
const OVERRIDE_METHODS = new Set( [ 'PATCH', 'PUT', 'DELETE' ] );

Expand All @@ -12,18 +15,17 @@ const OVERRIDE_METHODS = new Set( [ 'PATCH', 'PUT', 'DELETE' ] );
* is `GET`."
*
* @see https://fetch.spec.whatwg.org/#requests
*
* @type {string}
*/
const DEFAULT_METHOD = 'GET';

/**
* API Fetch middleware which overrides the request method for HTTP v1
* compatibility leveraging the REST API X-HTTP-Method-Override header.
*
* @type {import('../types').APIFetchMiddleware}
* @param options
* @param next
*/
const httpV1Middleware = ( options, next ) => {
const httpV1Middleware: APIFetchMiddleware = ( options, next ) => {
const { method = DEFAULT_METHOD } = options;
if ( OVERRIDE_METHODS.has( method.toUpperCase() ) ) {
options = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {
parseAndThrowError,
parseResponseAndNormalizeError,
} from '../utils/response';
import type { APIFetchOptions, APIFetchMiddleware } from '../types';

/**
* @param {import('../types').APIFetchOptions} options
* @return {boolean} True if the request is for media upload.
* @param options
* @return True if the request is for media upload.
*/
function isMediaUploadRequest( options ) {
function isMediaUploadRequest( options: APIFetchOptions ) {
const isCreateMethod = !! options.method && options.method === 'POST';
const isMediaEndpoint =
( !! options.path && options.path.indexOf( '/wp/v2/media' ) !== -1 ) ||
Expand All @@ -26,10 +27,10 @@ function isMediaUploadRequest( options ) {

/**
* Middleware handling media upload failures and retries.
*
* @type {import('../types').APIFetchMiddleware}
* @param options
* @param next
*/
const mediaUploadMiddleware = ( options, next ) => {
const mediaUploadMiddleware: APIFetchMiddleware = ( options, next ) => {
if ( ! isMediaUploadRequest( options ) ) {
return next( options );
}
Expand All @@ -38,10 +39,10 @@ const mediaUploadMiddleware = ( options, next ) => {
const maxRetries = 5;

/**
* @param {string} attachmentId
* @return {Promise<any>} Processed post response.
* @param attachmentId
* @return Processed post response.
*/
const postProcess = ( attachmentId ) => {
const postProcess = ( attachmentId: string ): Promise< any > => {
retries++;
return next( {
path: `/wp/v2/media/${ attachmentId }/post-process`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/**
* @type {import('../types').APIFetchMiddleware}
* Internal dependencies
*/
const namespaceAndEndpointMiddleware = ( options, next ) => {
import type { APIFetchMiddleware } from '../types';

const namespaceAndEndpointMiddleware: APIFetchMiddleware = (
options,
next
) => {
let path = options.path;
let namespaceTrimmed, endpointTrimmed;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
/**
* @param {string} nonce
* @return {import('../types').APIFetchMiddleware & { nonce: string }} A middleware to enhance a request with a nonce.
* Internal dependencies
*/
function createNonceMiddleware( nonce ) {
/**
* @type {import('../types').APIFetchMiddleware & { nonce: string }}
*/
const middleware = ( options, next ) => {
import type { APIFetchMiddleware } from '../types';

/**
* @param nonce
*
* @return A middleware to enhance a request with a nonce.
*/
function createNonceMiddleware(
nonce: string
): APIFetchMiddleware & { nonce: string } {
const middleware: APIFetchMiddleware & { nonce: string } = (
options,
next
) => {
const { headers = {} } = options;

// If an 'X-WP-Nonce' header (or any case-insensitive variation
Expand Down
Loading
Loading