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';
|
import { HTTPClientFactory } from '@woocommerce/api';
|
||||||
|
|
||||||
// You can create an API client using the client factory with pre-configured middleware for convenience.
|
// You can create an API client using the client factory with pre-configured middleware for convenience.
|
||||||
let httpClient = HTTPClientFactory.withBasicAuth(
|
let client = HTTPClientFactory.build( 'https://example.com' )
|
||||||
// The base URL of your REST API.
|
.withBasicAuth( 'username', 'password' )
|
||||||
'https://example.com/wp-json/',
|
.create();
|
||||||
// The username for your WordPress user.
|
|
||||||
'username',
|
|
||||||
// The password for your WordPress user.
|
|
||||||
'password',
|
|
||||||
);
|
|
||||||
|
|
||||||
// You can also create an API client configured for requests using OAuth.
|
// You can also create an API client configured for requests using OAuth.
|
||||||
httpClient = HTTPClientFactory.withOAuth(
|
client = HTTPClientFactory.build( 'https://example.com' )
|
||||||
// The base URL of your REST API.
|
.withOAuth( 'consumer_secret', 'consumer_password' )
|
||||||
'https://example.com/wp-json/',
|
.create();
|
||||||
// The OAuth API Key's consumer secret.
|
|
||||||
'consumer_secret',
|
|
||||||
// The OAuth API Key's consumer password.
|
|
||||||
'consumer_pasword',
|
|
||||||
);
|
|
||||||
|
|
||||||
// You can then use the client to make API requests.
|
// You can then use the client to make API requests.
|
||||||
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
||||||
|
@ -54,6 +44,7 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
||||||
}, ( error ) => {
|
}, ( error ) => {
|
||||||
// Handle errors that may have come up.
|
// Handle errors that may have come up.
|
||||||
} );
|
} );
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Repositories
|
### Repositories
|
||||||
|
@ -66,7 +57,9 @@ import { SimpleProduct } from '@woocommerce/api';
|
||||||
|
|
||||||
// Prepare the HTTP client that will be consumed by the repository.
|
// Prepare the HTTP client that will be consumed by the repository.
|
||||||
// This is necessary so that it can make requests to the REST API.
|
// 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 );
|
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', () => {
|
describe( 'buildURL', () => {
|
||||||
it( 'should use base when given no url', () => {
|
it( 'should use base when given no url', () => {
|
||||||
|
@ -15,9 +15,16 @@ describe( 'buildURL', () => {
|
||||||
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
|
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
|
||||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||||
} );
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
it( 'should combine base and url with trailing/leading slashes', () => {
|
describe( 'buildURLWithParams', () => {
|
||||||
const url = buildURL( { baseURL: 'http://test.test/////', url: '////yes/test' } );
|
it( 'should do nothing without query string', () => {
|
||||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
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 createHmac from 'create-hmac';
|
||||||
import * as OAuth from 'oauth-1.0a';
|
import * as OAuth from 'oauth-1.0a';
|
||||||
import { AxiosInterceptor } from './axios-interceptor';
|
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 {
|
export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||||
/**
|
/**
|
||||||
|
@ -14,7 +14,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private oauth: OAuth;
|
private readonly oauth: OAuth;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new interceptor.
|
* Creates a new interceptor.
|
||||||
|
@ -44,7 +44,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||||
* @return {AxiosRequestConfig} The request with the additional authorization headers.
|
* @return {AxiosRequestConfig} The request with the additional authorization headers.
|
||||||
*/
|
*/
|
||||||
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
|
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
|
||||||
const url = buildURL( request );
|
const url = buildURLWithParams( request );
|
||||||
if ( url.startsWith( 'https' ) ) {
|
if ( url.startsWith( 'https' ) ) {
|
||||||
request.auth = {
|
request.auth = {
|
||||||
username: this.oauth.consumer.key,
|
username: this.oauth.consumer.key,
|
||||||
|
|
|
@ -2,6 +2,9 @@ import { AxiosResponse } from 'axios';
|
||||||
import { AxiosInterceptor } from './axios-interceptor';
|
import { AxiosInterceptor } from './axios-interceptor';
|
||||||
import { HTTPResponse } from '../http-client';
|
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 {
|
export class AxiosResponseInterceptor extends AxiosInterceptor {
|
||||||
/**
|
/**
|
||||||
* Transforms the Axios response into our HTTP response.
|
* 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';
|
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
|
* Given an Axios request config this function generates the URL that Axios will
|
||||||
* use to make the request.
|
* use to make the request.
|
||||||
|
@ -8,17 +13,16 @@ import { AxiosRequestConfig } from 'axios';
|
||||||
* @return {string} The merged URL.
|
* @return {string} The merged URL.
|
||||||
*/
|
*/
|
||||||
export function buildURL( request: AxiosRequestConfig ): string {
|
export function buildURL( request: AxiosRequestConfig ): string {
|
||||||
const base = request.baseURL || '';
|
return buildFullPath( request.baseURL, request.url );
|
||||||
if ( ! request.url ) {
|
}
|
||||||
return base;
|
|
||||||
}
|
/**
|
||||||
|
* Given an Axios request config this function generates the URL that Axios will
|
||||||
// Axios ignores the base when the URL is absolute.
|
* use to make the request with the query parameters included.
|
||||||
const url = request.url;
|
*
|
||||||
if ( ! base || url.match( /^([a-z][a-z\d+\-.]*:)?\/\/[^\/]/i ) ) {
|
* @param {AxiosRequestConfig} request The Axios request we're building the URL for.
|
||||||
return url;
|
* @return {string} The merged URL.
|
||||||
}
|
*/
|
||||||
|
export function buildURLWithParams( request: AxiosRequestConfig ): string {
|
||||||
// Remove trailing slashes from the base and leading slashes from the URL so we can combine them consistently.
|
return appendParams( buildURL( request ), request.params, request.paramsSerializer );
|
||||||
return base.replace( /\/+$/, '' ) + '/' + url.replace( /^\/+/, '' );
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,141 @@
|
||||||
import { HTTPClient } from './http-client';
|
import { HTTPClient } from './http-client';
|
||||||
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
|
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 {
|
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
|
* @private
|
||||||
* @param {string} username
|
|
||||||
* @param {string} password
|
|
||||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
|
||||||
*/
|
*/
|
||||||
public static withBasicAuth( apiURL: string, username: string, password: string ): HTTPClient {
|
private clientConfig: BuildParams;
|
||||||
return new AxiosClient(
|
|
||||||
{
|
private constructor( wpURL: string ) {
|
||||||
baseURL: apiURL,
|
this.clientConfig = { wpURL };
|
||||||
auth: { username, password },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new client instance prepared for oauth.
|
* Creates a new factory that can be used to build clients.
|
||||||
*
|
*
|
||||||
* @param {string} apiURL
|
* @param {string} wpURL The root URL of the WordPress installation we're querying.
|
||||||
* @param {string} consumerKey
|
* @return {HTTPClientFactory} The new factory instance.
|
||||||
* @param {string} consumerSecret
|
|
||||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
|
||||||
*/
|
*/
|
||||||
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
|
public static build( wpURL: string ): HTTPClientFactory {
|
||||||
return new AxiosClient(
|
return new HTTPClientFactory( wpURL );
|
||||||
{ baseURL: apiURL },
|
}
|
||||||
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
|
|
||||||
);
|
/**
|
||||||
|
* 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';
|
import { HTTPClientFactory } from '@woocommerce/api';
|
||||||
const config = require( 'config' );
|
const config = require( 'config' );
|
||||||
|
|
||||||
const httpClient = HTTPClientFactory.withBasicAuth(
|
const httpClient = HTTPClientFactory.build( config.get( 'url' ) )
|
||||||
config.get( 'url' ) + '/wp-json',
|
.withBasicAuth( config.get( 'users.admin.username' ), config.get( 'users.admin.password' ) )
|
||||||
config.get( 'users.admin.username' ),
|
.create();
|
||||||
config.get( 'users.admin.password' ),
|
|
||||||
);
|
|
||||||
|
|
||||||
import { simpleProductFactory } from './factories/simple-product';
|
import { simpleProductFactory } from './factories/simple-product';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue