Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert api-fetch package to TypeScript #67669

Open
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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