mirror of
https://github.com/vercel/commerce.git
synced 2025-06-28 09:21:22 +00:00
430 lines
11 KiB
TypeScript
430 lines
11 KiB
TypeScript
import axios, { AxiosRequestConfig, RawAxiosRequestHeaders } from 'axios';
|
|
import crypto from 'node:crypto';
|
|
import OAuth from 'oauth-1.0a';
|
|
import Url from 'url-parse';
|
|
import { DELETE, IWCRestApiOptions, WCRestApiEndpoint, WCRestApiMethod } from './clientOptions';
|
|
|
|
/**
|
|
* Set the axiosConfig property to the axios config object.
|
|
* Could reveive any axios |... config objects.
|
|
* @param {AxiosRequestConfig} axiosConfig
|
|
*/
|
|
export type WCRestApiOptions = IWCRestApiOptions<AxiosRequestConfig>;
|
|
|
|
/**
|
|
* Set all the possible query params for the WCCommerce REST API.
|
|
*/
|
|
export type WCRestApiParams = DELETE;
|
|
|
|
/**
|
|
* Define the response types for each endpoint.
|
|
*/
|
|
type WCCommerceResponse<
|
|
T extends WCRestApiEndpoint,
|
|
P extends Partial<WCRestApiParams> = {}
|
|
> = P['id'] extends number | string ? (T extends 'posts' ? any : any) : any;
|
|
|
|
/**
|
|
* WCCommerce REST API wrapper
|
|
*
|
|
* @param {Object} opt
|
|
*/
|
|
export default class WCCommerceRestApi<T extends WCRestApiOptions> {
|
|
protected _opt: T;
|
|
|
|
/**
|
|
* Class constructor.
|
|
*
|
|
* @param {Object} opt
|
|
*/
|
|
constructor(opt: T) {
|
|
this._opt = opt;
|
|
|
|
/**
|
|
* If the class is not instantiated, return a new instance.
|
|
* This is useful for the static methods.
|
|
*/
|
|
if (!(this instanceof WCCommerceRestApi)) {
|
|
return new WCCommerceRestApi(opt);
|
|
}
|
|
|
|
/**
|
|
* Check if the url is defined.
|
|
*/
|
|
if (!this._opt.url || this._opt.url === '') {
|
|
throw new OptionsException('url is required');
|
|
}
|
|
|
|
/**
|
|
* Check if the consumerKey is defined.
|
|
*/
|
|
if (!this._opt.consumerKey || this._opt.consumerKey === '') {
|
|
throw new OptionsException('consumerKey is required');
|
|
}
|
|
|
|
/**
|
|
* Check if the consumerSecret is defined.
|
|
*/
|
|
if (!this._opt.consumerSecret || this._opt.consumerSecret === '') {
|
|
throw new OptionsException('consumerSecret is required');
|
|
}
|
|
|
|
/**
|
|
* Set default options
|
|
*/
|
|
this._setDefaultsOptions(this._opt);
|
|
}
|
|
|
|
/**
|
|
* Set default options
|
|
*
|
|
* @param {Object} opt
|
|
*/
|
|
_setDefaultsOptions(opt: T): void {
|
|
this._opt.wpAPIPrefix = opt.wpAPIPrefix || 'wp-json';
|
|
this._opt.version = opt.version || 'wp/v2';
|
|
this._opt.isHttps = /^https/i.test(this._opt.url);
|
|
this._opt.encoding = opt.encoding || 'utf-8';
|
|
this._opt.queryStringAuth = opt.queryStringAuth || false;
|
|
this._opt.classVersion = '0.0.2';
|
|
}
|
|
|
|
login(username: string, password: string): Promise<any> {
|
|
return this._request('POST', 'token', { username, password }, {}, 'jwt-auth/v1');
|
|
}
|
|
|
|
/**
|
|
* Parse params to object.
|
|
*
|
|
* @param {Object} params
|
|
* @param {Object} query
|
|
* @return {Object} IWCRestApiQuery
|
|
*/
|
|
// _parseParamsObject<T>(params: Record<string, T>, query: Record<string, any>): IWCRestApiQuery {
|
|
// for (const key in params) {
|
|
// if (typeof params[key] === "object") {
|
|
// // If the value is an object, loop through it and add it to the query object
|
|
// for (const subKey in params[key]) {
|
|
// query[key + "[" + subKey + "]"] = params[key][subKey];
|
|
// }
|
|
// } else {
|
|
// query[key] = params[key]; // If the value is not an object, add it to the query object
|
|
// }
|
|
// }
|
|
// return query; // Return the query object
|
|
// }
|
|
|
|
/**
|
|
* Normalize query string for oAuth 1.0a
|
|
* Depends on the _parseParamsObject method
|
|
*
|
|
* @param {String} url
|
|
* @param {Object} params
|
|
*
|
|
* @return {String}
|
|
*/
|
|
_normalizeQueryString(url: string, params: Partial<Record<string, any>>): string {
|
|
/**
|
|
* Exit if url and params are not defined
|
|
*/
|
|
if (url.indexOf('?') === -1 && Object.keys(params).length === 0) {
|
|
return url;
|
|
}
|
|
const query = new Url(url, true).query; // Parse the query string returned by the url
|
|
|
|
const values = [];
|
|
|
|
let queryString = '';
|
|
|
|
// Include params object into URL.searchParams.
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
// const a = this._parseParamsObject(params, query);
|
|
// console.log("A:", a);
|
|
|
|
/**
|
|
* Loop through the params object and push the key and value into the values array
|
|
* Example: values = ['key1=value1', 'key2=value2']
|
|
*/
|
|
for (const key in query) {
|
|
values.push(key);
|
|
}
|
|
|
|
values.sort(); // Sort the values array
|
|
|
|
for (const i in values) {
|
|
/*
|
|
* If the queryString is not empty, add an ampersand to the end of the string
|
|
*/
|
|
if (queryString.length) queryString += '&';
|
|
|
|
/**
|
|
* Add the key and value to the queryString
|
|
*/
|
|
queryString +=
|
|
encodeURIComponent(values[i] || '') +
|
|
'=' +
|
|
encodeURIComponent(<string | number | boolean>(query[values[i] as string] ?? ''));
|
|
}
|
|
/**
|
|
* Replace %5B with [ and %5D with ]
|
|
*/
|
|
queryString = queryString.replace(/%5B/g, '[').replace(/%5D/g, ']');
|
|
|
|
/**
|
|
* Return the url with the queryString
|
|
*/
|
|
const urlObject = url.split('?')[0] + '?' + queryString;
|
|
|
|
return urlObject;
|
|
}
|
|
|
|
/**
|
|
* Get URL
|
|
*
|
|
* @param {String} endpoint
|
|
* @param {Object} params
|
|
*
|
|
* @return {String}
|
|
*/
|
|
_getUrl(endpoint: string, params: Partial<Record<string, unknown>>, version?: string): string {
|
|
const api = this._opt.wpAPIPrefix + '/'; // Add prefix to endpoint
|
|
|
|
let url = this._opt.url.slice(-1) === '/' ? this._opt.url : this._opt.url + '/';
|
|
|
|
url = url + api + (version ?? this._opt.version) + '/' + endpoint;
|
|
// Add id param to url
|
|
if (params.id) {
|
|
url = url + '/' + params.id;
|
|
delete params.id;
|
|
}
|
|
|
|
const queryParams: string[] = [];
|
|
for (const key in params) {
|
|
queryParams.push(
|
|
`${encodeURIComponent(key)}=${encodeURIComponent(params[key] as string | number | boolean)}`
|
|
);
|
|
}
|
|
|
|
if (queryParams.length > 0) {
|
|
url += '?' + queryParams.join('&');
|
|
}
|
|
|
|
/**
|
|
* If port is defined, add it to the url
|
|
*/
|
|
if (this._opt.port) {
|
|
const hostname = new Url(url).hostname;
|
|
url = url.replace(hostname, hostname + ':' + this._opt.port);
|
|
}
|
|
|
|
/**
|
|
* If isHttps is true, normalize the query string
|
|
*/
|
|
// if (this._opt.isHttps) {
|
|
// url = this._normalizeQueryString(url, params);
|
|
// return url;
|
|
// }
|
|
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Create Hmac was deprecated fot this version at 16.11.2022
|
|
* Get OAuth 1.0a since it is mandatory for WCCommerce REST API
|
|
* You must use OAuth 1.0a "one-legged" authentication to ensure REST API credentials cannot be intercepted by an attacker.
|
|
* Reference: https://WCcommerce.github.io/WCcommerce-rest-api-docs/#authentication-over-http
|
|
* @return {Object}
|
|
*/
|
|
_getOAuth(): OAuth {
|
|
const data = {
|
|
consumer: {
|
|
key: this._opt.consumerKey,
|
|
secret: this._opt.consumerSecret
|
|
},
|
|
signature_method: 'HMAC-SHA256',
|
|
hash_function: (base: any, key: any) => {
|
|
return crypto.createHmac('sha256', key).update(base).digest('base64');
|
|
}
|
|
};
|
|
|
|
return new OAuth(data);
|
|
}
|
|
|
|
/**
|
|
* Axios request
|
|
* Mount the options to send to axios and send the request.
|
|
*
|
|
* @param {String} method
|
|
* @param {String} endpoint
|
|
* @param {Object} data
|
|
* @param {Object} params
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
_request<T extends WCRestApiEndpoint, P extends Partial<WCRestApiParams>>(
|
|
method: WCRestApiMethod,
|
|
endpoint: T,
|
|
data?: Record<string, unknown>,
|
|
params: P = {} as P,
|
|
version?: string
|
|
): Promise<WCCommerceResponse<T, P>> {
|
|
const url = this._getUrl(endpoint, params, version);
|
|
const header: RawAxiosRequestHeaders = {
|
|
Accept: 'application/json'
|
|
};
|
|
// only set "User-Agent" in node environment
|
|
// the checking method is identical to upstream axios
|
|
if (
|
|
typeof process !== 'undefined' &&
|
|
Object.prototype.toString.call(process) === '[object process]'
|
|
) {
|
|
header['User-Agent'] = 'WCCommerce REST API - TS Client/' + this._opt.classVersion;
|
|
}
|
|
|
|
let options: AxiosRequestConfig = {
|
|
url,
|
|
method,
|
|
responseEncoding: this._opt.encoding,
|
|
timeout: this._opt.timeout,
|
|
responseType: 'json',
|
|
headers: { ...header },
|
|
params: {},
|
|
data: data ? JSON.stringify(data) : null
|
|
};
|
|
|
|
/**
|
|
* If isHttps is false, add the query string to the params object
|
|
*/
|
|
if (this._opt.isHttps) {
|
|
if (this._opt.queryStringAuth) {
|
|
options.params = {
|
|
consumer_key: this._opt.consumerKey,
|
|
consumer_secret: this._opt.consumerSecret
|
|
};
|
|
} else {
|
|
options.auth = {
|
|
username: this._opt.consumerKey,
|
|
password: this._opt.consumerSecret
|
|
};
|
|
}
|
|
|
|
options.params = { ...options.params, ...params };
|
|
} else {
|
|
options.params = this._getOAuth().authorize({
|
|
url,
|
|
method
|
|
});
|
|
}
|
|
|
|
if (options.data) {
|
|
options.headers = {
|
|
...header,
|
|
'Content-Type': `application/json; charset=${this._opt.encoding}`
|
|
};
|
|
}
|
|
|
|
// Allow set and override Axios options.
|
|
options = { ...options, ...this._opt.axiosConfig };
|
|
|
|
return axios(options).then((response) => response.data as WCCommerceResponse<T, P>);
|
|
}
|
|
|
|
/**
|
|
* GET requests
|
|
*
|
|
* @param {String} endpoint
|
|
* @param {Object} params
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
get<T extends WCRestApiEndpoint, P extends Partial<WCRestApiParams>>(
|
|
endpoint: T,
|
|
params?: P
|
|
): Promise<WCCommerceResponse<T, P>> {
|
|
return this._request('GET', endpoint, undefined, params || ({} as P));
|
|
}
|
|
|
|
/**
|
|
* POST requests
|
|
*
|
|
* @param {String} endpoint
|
|
* @param {Object} data
|
|
* @param {Object} params
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
post<T extends WCRestApiEndpoint>(
|
|
endpoint: T,
|
|
data: Record<string, unknown>,
|
|
params?: Partial<WCRestApiParams>
|
|
): Promise<WCCommerceResponse<T>> {
|
|
return this._request('POST', endpoint, data, params);
|
|
}
|
|
|
|
/**
|
|
* PUT requests
|
|
*
|
|
* @param {String} endpoint
|
|
* @param {Object} data
|
|
* @param {Object} params
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
put<T extends WCRestApiEndpoint>(
|
|
endpoint: T,
|
|
data: Record<string, unknown>,
|
|
params?: Partial<WCRestApiParams>
|
|
): Promise<WCCommerceResponse<T>> {
|
|
return this._request('PUT', endpoint, data, params);
|
|
}
|
|
|
|
/**
|
|
* DELETE requests
|
|
*
|
|
* @param {String} endpoint
|
|
* @param {Object} params
|
|
* @param {Object} params
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
delete<T extends WCRestApiEndpoint>(
|
|
endpoint: T,
|
|
data: Pick<WCRestApiParams, 'force'>,
|
|
params: Pick<WCRestApiParams, 'id'>
|
|
): Promise<WCCommerceResponse<T, Pick<WCRestApiParams, 'id'>>> {
|
|
return this._request('DELETE', endpoint, data, params);
|
|
}
|
|
|
|
/**
|
|
* OPTIONS requests
|
|
*
|
|
* @param {String} endpoint
|
|
* @param {Object} params
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
options<T extends WCRestApiEndpoint>(
|
|
endpoint: T,
|
|
params?: Partial<WCRestApiParams>
|
|
): Promise<WCCommerceResponse<T>> {
|
|
return this._request('OPTIONS', endpoint, {}, params);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Options Exception.
|
|
*/
|
|
export class OptionsException {
|
|
public name: 'Options Error';
|
|
public message: string;
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param {String} message
|
|
*/
|
|
constructor(message: string) {
|
|
this.name = 'Options Error';
|
|
this.message = message;
|
|
}
|
|
}
|