Merge pull request #28186 from woocommerce/packages/api/fix/pretty-permalinks-and-http-status
@woocommerce/api: Better support pretty permalinks
This commit is contained in:
commit
5f7454ae18
|
@ -24,24 +24,14 @@ The simplest way to use the client is directly:
|
|||
import { HTTPClientFactory } from '@woocommerce/api';
|
||||
|
||||
// You can create an API client using the client factory with pre-configured middleware for convenience.
|
||||
let httpClient = HTTPClientFactory.withBasicAuth(
|
||||
// The base URL of your REST API.
|
||||
'https://example.com/wp-json/',
|
||||
// The username for your WordPress user.
|
||||
'username',
|
||||
// The password for your WordPress user.
|
||||
'password',
|
||||
);
|
||||
let client = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withBasicAuth( 'username', 'password' )
|
||||
.create();
|
||||
|
||||
// You can also create an API client configured for requests using OAuth.
|
||||
httpClient = HTTPClientFactory.withOAuth(
|
||||
// The base URL of your REST API.
|
||||
'https://example.com/wp-json/',
|
||||
// The OAuth API Key's consumer secret.
|
||||
'consumer_secret',
|
||||
// The OAuth API Key's consumer password.
|
||||
'consumer_pasword',
|
||||
);
|
||||
client = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withOAuth( 'consumer_secret', 'consumer_password' )
|
||||
.create();
|
||||
|
||||
// You can then use the client to make API requests.
|
||||
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
||||
|
@ -54,6 +44,7 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
|||
}, ( error ) => {
|
||||
// Handle errors that may have come up.
|
||||
} );
|
||||
|
||||
```
|
||||
|
||||
### Repositories
|
||||
|
@ -66,7 +57,9 @@ import { SimpleProduct } from '@woocommerce/api';
|
|||
|
||||
// Prepare the HTTP client that will be consumed by the repository.
|
||||
// This is necessary so that it can make requests to the REST API.
|
||||
const httpClient = HTTPClientFactory.withBasicAuth( 'https://example.com/wp-json/','username','password' );
|
||||
const httpClient = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withBasicAuth( 'username', 'password' )
|
||||
.create();
|
||||
|
||||
const repository = SimpleProduct.restRepository( httpClient );
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
import * as moxios from 'moxios';
|
||||
import { AxiosURLToQueryInterceptor } from '../axios-url-to-query-interceptor';
|
||||
|
||||
describe( 'AxiosURLToQueryInterceptor', () => {
|
||||
let urlToQueryInterceptor: AxiosURLToQueryInterceptor;
|
||||
let axiosInstance: AxiosInstance;
|
||||
|
||||
beforeEach( () => {
|
||||
axiosInstance = axios.create();
|
||||
moxios.install( axiosInstance );
|
||||
urlToQueryInterceptor = new AxiosURLToQueryInterceptor( 'test' );
|
||||
urlToQueryInterceptor.start( axiosInstance );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
urlToQueryInterceptor.stop( axiosInstance );
|
||||
moxios.uninstall();
|
||||
} );
|
||||
|
||||
it( 'should put path in query string', async () => {
|
||||
moxios.stubRequest( 'http://test.test/?test=%2Ftest%2Froute', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseText: JSON.stringify( { test: 'value' } ),
|
||||
} );
|
||||
|
||||
const response = await axiosInstance.get( 'http://test.test/test/route' );
|
||||
|
||||
expect( response.status ).toEqual( 200 );
|
||||
} );
|
||||
} );
|
|
@ -1,4 +1,4 @@
|
|||
import { buildURL } from '../utils';
|
||||
import { buildURL, buildURLWithParams } from '../utils';
|
||||
|
||||
describe( 'buildURL', () => {
|
||||
it( 'should use base when given no url', () => {
|
||||
|
@ -15,9 +15,16 @@ describe( 'buildURL', () => {
|
|||
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
|
||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should combine base and url with trailing/leading slashes', () => {
|
||||
const url = buildURL( { baseURL: 'http://test.test/////', url: '////yes/test' } );
|
||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||
describe( 'buildURLWithParams', () => {
|
||||
it( 'should do nothing without query string', () => {
|
||||
const url = buildURLWithParams( { baseURL: 'http://test.test' } );
|
||||
expect( url ).toBe( 'http://test.test' );
|
||||
} );
|
||||
|
||||
it( 'should append query string', () => {
|
||||
const url = buildURLWithParams( { baseURL: 'http://test.test', params: { test: 'yes' } } );
|
||||
expect( url ).toBe( 'http://test.test?test=yes' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -2,10 +2,10 @@ import type { AxiosRequestConfig } from 'axios';
|
|||
import * as createHmac from 'create-hmac';
|
||||
import * as OAuth from 'oauth-1.0a';
|
||||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { buildURL } from './utils';
|
||||
import { buildURLWithParams } from './utils';
|
||||
|
||||
/**
|
||||
* A utility class for managing the lifecycle of an authentication interceptor.
|
||||
* An interceptor for adding OAuth 1.0a signatures to HTTP requests.
|
||||
*/
|
||||
export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
|||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
private oauth: OAuth;
|
||||
private readonly oauth: OAuth;
|
||||
|
||||
/**
|
||||
* Creates a new interceptor.
|
||||
|
@ -44,7 +44,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
|||
* @return {AxiosRequestConfig} The request with the additional authorization headers.
|
||||
*/
|
||||
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
|
||||
const url = buildURL( request );
|
||||
const url = buildURLWithParams( request );
|
||||
if ( url.startsWith( 'https' ) ) {
|
||||
request.auth = {
|
||||
username: this.oauth.consumer.key,
|
||||
|
|
|
@ -2,6 +2,9 @@ import { AxiosResponse } from 'axios';
|
|||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { HTTPResponse } from '../http-client';
|
||||
|
||||
/**
|
||||
* An interceptor for transforming the responses from axios into a consistent format for package consumers.
|
||||
*/
|
||||
export class AxiosResponseInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
* Transforms the Axios response into our HTTP response.
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { buildURL } from './utils';
|
||||
|
||||
/**
|
||||
* An interceptor for transforming the request's path into a query parameter.
|
||||
*/
|
||||
export class AxiosURLToQueryInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
* The query parameter we want to assign the path to.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
private readonly queryParam: string;
|
||||
|
||||
/**
|
||||
* Constructs a new interceptor.
|
||||
*
|
||||
* @param {string} queryParam The query parameter we want to assign the path to.
|
||||
*/
|
||||
public constructor( queryParam: string ) {
|
||||
super();
|
||||
|
||||
this.queryParam = queryParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the outgoing path into a query parameter.
|
||||
*
|
||||
* @param {AxiosRequestConfig} config The axios config.
|
||||
* @return {AxiosRequestConfig} The axios config.
|
||||
*/
|
||||
protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig {
|
||||
const url = new URL( buildURL( config ) );
|
||||
|
||||
// Store the path in the query string.
|
||||
if ( config.params instanceof URLSearchParams ) {
|
||||
config.params.set( this.queryParam, url.pathname );
|
||||
} else if ( config.params ) {
|
||||
config.params[ this.queryParam ] = url.pathname;
|
||||
} else {
|
||||
config.params = { [ this.queryParam ]: url.pathname };
|
||||
}
|
||||
|
||||
// Store the URL without the path now that it's in the query string.
|
||||
url.pathname = '';
|
||||
config.url = url.toString();
|
||||
delete config.baseURL;
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
|
||||
// @ts-ignore
|
||||
import buildFullPath = require( 'axios/lib/core/buildFullPath' );
|
||||
// @ts-ignore
|
||||
import appendParams = require( 'axios/lib/helpers/buildURL' );
|
||||
|
||||
/**
|
||||
* Given an Axios request config this function generates the URL that Axios will
|
||||
* use to make the request.
|
||||
|
@ -8,17 +13,16 @@ import { AxiosRequestConfig } from 'axios';
|
|||
* @return {string} The merged URL.
|
||||
*/
|
||||
export function buildURL( request: AxiosRequestConfig ): string {
|
||||
const base = request.baseURL || '';
|
||||
if ( ! request.url ) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// Axios ignores the base when the URL is absolute.
|
||||
const url = request.url;
|
||||
if ( ! base || url.match( /^([a-z][a-z\d+\-.]*:)?\/\/[^\/]/i ) ) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Remove trailing slashes from the base and leading slashes from the URL so we can combine them consistently.
|
||||
return base.replace( /\/+$/, '' ) + '/' + url.replace( /^\/+/, '' );
|
||||
return buildFullPath( request.baseURL, request.url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Axios request config this function generates the URL that Axios will
|
||||
* use to make the request with the query parameters included.
|
||||
*
|
||||
* @param {AxiosRequestConfig} request The Axios request we're building the URL for.
|
||||
* @return {string} The merged URL.
|
||||
*/
|
||||
export function buildURLWithParams( request: AxiosRequestConfig ): string {
|
||||
return appendParams( buildURL( request ), request.params, request.paramsSerializer );
|
||||
}
|
||||
|
|
|
@ -1,39 +1,141 @@
|
|||
import { HTTPClient } from './http-client';
|
||||
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { AxiosInterceptor } from './axios/axios-interceptor';
|
||||
import { AxiosURLToQueryInterceptor } from './axios/axios-url-to-query-interceptor';
|
||||
|
||||
/**
|
||||
* A class for generating HTTPClient instances with desired configurations.
|
||||
* These types describe the shape of the different auth methods our factory supports.
|
||||
*/
|
||||
type OAuthMethod = {
|
||||
type: 'oauth',
|
||||
key: string,
|
||||
secret: string,
|
||||
};
|
||||
type BasicAuthMethod = {
|
||||
type: 'basic',
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for describing the shape of a client to create using the factory.
|
||||
*/
|
||||
interface BuildParams {
|
||||
wpURL: string,
|
||||
useIndexPermalinks?: boolean,
|
||||
auth?: OAuthMethod | BasicAuthMethod,
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory for generating an HTTPClient with a desired configuration.
|
||||
*/
|
||||
export class HTTPClientFactory {
|
||||
/**
|
||||
* Creates a new client instance prepared for basic auth.
|
||||
* The configuration object describing the client we're trying to create.
|
||||
*
|
||||
* @param {string} apiURL
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
||||
* @private
|
||||
*/
|
||||
public static withBasicAuth( apiURL: string, username: string, password: string ): HTTPClient {
|
||||
return new AxiosClient(
|
||||
{
|
||||
baseURL: apiURL,
|
||||
auth: { username, password },
|
||||
},
|
||||
);
|
||||
private clientConfig: BuildParams;
|
||||
|
||||
private constructor( wpURL: string ) {
|
||||
this.clientConfig = { wpURL };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client instance prepared for oauth.
|
||||
* Creates a new factory that can be used to build clients.
|
||||
*
|
||||
* @param {string} apiURL
|
||||
* @param {string} consumerKey
|
||||
* @param {string} consumerSecret
|
||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
||||
* @param {string} wpURL The root URL of the WordPress installation we're querying.
|
||||
* @return {HTTPClientFactory} The new factory instance.
|
||||
*/
|
||||
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
|
||||
return new AxiosClient(
|
||||
{ baseURL: apiURL },
|
||||
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
|
||||
);
|
||||
public static build( wpURL: string ): HTTPClientFactory {
|
||||
return new HTTPClientFactory( wpURL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to utilize OAuth.
|
||||
*
|
||||
* @param {string} key The OAuth consumer key to use.
|
||||
* @param {string} secret The OAuth consumer secret to use.
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withOAuth( key: string, secret: string ): this {
|
||||
this.clientConfig.auth = { type: 'oauth', key, secret };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to utilize basic auth.
|
||||
*
|
||||
* @param {string} username The WordPress username to use.
|
||||
* @param {string} password The password for the WordPress user.
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withBasicAuth( username: string, password: string ): this {
|
||||
this.clientConfig.auth = { type: 'basic', username, password };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to use index permalinks.
|
||||
*
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withIndexPermalinks(): this {
|
||||
this.clientConfig.useIndexPermalinks = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to use query permalinks.
|
||||
*
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withoutIndexPermalinks(): this {
|
||||
this.clientConfig.useIndexPermalinks = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client instance using the configuration stored within.
|
||||
*
|
||||
* @return {HTTPClient} The created client.
|
||||
*/
|
||||
public create(): HTTPClient {
|
||||
const axiosConfig: AxiosRequestConfig = {};
|
||||
const interceptors: AxiosInterceptor[] = [];
|
||||
|
||||
axiosConfig.baseURL = this.clientConfig.wpURL;
|
||||
if ( ! axiosConfig.baseURL.endsWith( '/' ) ) {
|
||||
axiosConfig.baseURL += '/';
|
||||
}
|
||||
|
||||
if ( this.clientConfig.useIndexPermalinks ) {
|
||||
axiosConfig.baseURL += 'wp-json/';
|
||||
} else {
|
||||
interceptors.push( new AxiosURLToQueryInterceptor( 'rest_route' ) );
|
||||
}
|
||||
|
||||
if ( this.clientConfig.auth ) {
|
||||
switch ( this.clientConfig.auth.type ) {
|
||||
case 'basic':
|
||||
axiosConfig.auth = {
|
||||
username: this.clientConfig.auth.username,
|
||||
password: this.clientConfig.auth.password,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'oauth':
|
||||
interceptors.push(
|
||||
new AxiosOAuthInterceptor(
|
||||
this.clientConfig.auth.key,
|
||||
this.clientConfig.auth.secret,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new AxiosClient( axiosConfig, interceptors );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { HTTPClientFactory } from '@woocommerce/api';
|
||||
const config = require( 'config' );
|
||||
|
||||
const httpClient = HTTPClientFactory.withBasicAuth(
|
||||
config.get( 'url' ) + '/wp-json',
|
||||
config.get( 'users.admin.username' ),
|
||||
config.get( 'users.admin.password' ),
|
||||
);
|
||||
const httpClient = HTTPClientFactory.build( config.get( 'url' ) )
|
||||
.withBasicAuth( config.get( 'users.admin.username' ), config.get( 'users.admin.password' ) )
|
||||
.create();
|
||||
|
||||
import { simpleProductFactory } from './factories/simple-product';
|
||||
|
||||
|
|
Loading…
Reference in New Issue