package lock maintenance

This commit is contained in:
Ron Rennick 2020-10-05 09:31:48 -03:00
commit 610e787a86
61 changed files with 6259 additions and 6040 deletions

View File

@ -15,7 +15,7 @@
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "1.6.0-beta.1",
"woocommerce/woocommerce-admin": "1.6.0-rc.3",
"woocommerce/woocommerce-blocks": "3.4.0"
},
"require-dev": {

24
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ddfcf89afffda07ac78adfff8b311224",
"content-hash": "dcf828ebdcdcecfa605e7d3516f6e769",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -380,7 +380,7 @@
},
{
"name": "symfony/css-selector",
"version": "v3.4.44",
"version": "v3.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@ -468,16 +468,16 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "1.6.0-beta.1",
"version": "1.6.0-rc.3",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "a03cafd0a218451d83c42285b02f797555a7450e"
"reference": "a2d0e41675f9c44d49e02fe6ed44310987a4ce88"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/a03cafd0a218451d83c42285b02f797555a7450e",
"reference": "a03cafd0a218451d83c42285b02f797555a7450e",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/a2d0e41675f9c44d49e02fe6ed44310987a4ce88",
"reference": "a2d0e41675f9c44d49e02fe6ed44310987a4ce88",
"shasum": ""
},
"require": {
@ -511,7 +511,7 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2020-09-18T15:24:50+00:00"
"time": "2020-10-01T13:28:06+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -2424,16 +2424,16 @@
},
{
"name": "symfony/finder",
"version": "v3.4.44",
"version": "v3.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "5ec813ccafa8164ef21757e8c725d3a57da59200"
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/5ec813ccafa8164ef21757e8c725d3a57da59200",
"reference": "5ec813ccafa8164ef21757e8c725d3a57da59200",
"url": "https://api.github.com/repos/symfony/finder/zipball/52140652ed31cee3dabd0c481b5577201fa769b4",
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4",
"shasum": ""
},
"require": {
@ -2469,7 +2469,7 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2020-02-14T07:34:21+00:00"
"time": "2020-09-02T16:06:40+00:00"
},
{
"name": "symfony/polyfill-ctype",

View File

@ -304,6 +304,7 @@ class WC_Install {
self::create_cron_jobs();
self::create_files();
self::maybe_create_pages();
self::maybe_set_activation_transients();
self::update_wc_version();
self::maybe_update_db_version();
@ -403,6 +404,17 @@ class WC_Install {
return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' );
}
/**
* See if we need to set redirect transients for activation or not.
*
* @since 4.6.0
*/
private static function maybe_set_activation_transients() {
if ( self::is_new_install() ) {
set_transient( '_wc_activation_redirect', 1, 30 );
}
}
/**
* See if we need to show or run database updates during install.
*

5034
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,10 +40,10 @@
"@typescript-eslint/eslint-plugin": "3.1.0",
"@typescript-eslint/experimental-utils": "^2.34.0",
"@typescript-eslint/parser": "3.1.0",
"@woocommerce/api": "file:tests/e2e/api",
"@woocommerce/e2e-core-tests": "file:tests/e2e/core-tests",
"@woocommerce/e2e-environment": "file:tests/e2e/env",
"@woocommerce/e2e-utils": "file:tests/e2e/utils",
"@woocommerce/model-factories": "file:tests/e2e/factories",
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
"@wordpress/babel-preset-default": "3.0.2",
"@wordpress/e2e-test-utils": "4.6.0",

View File

@ -10,13 +10,22 @@ module.exports = {
rules: {
'no-unused-vars': 'off',
'no-dupe-class-members': 'off',
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 2,
},
'plugins': [
'@typescript-eslint'
],
extends: [
'plugin:@wordpress/eslint-plugin/recommended-with-formatting'
],
overrides: [
{
'files': [ '**/*.ts' ]
'files': [
'**/*.js',
'**/*.ts'
]
},
{
'files': [

77
tests/e2e/api/README.md Normal file
View File

@ -0,0 +1,77 @@
# WooCommerce API Client
An isometric API client for interacting with WooCommerce installations. Here are the current and planned
features:
- [x] TypeScript Definitions
- [x] Axios API Client with support for OAuth & basic auth
- [x] Repositories to simplify interaction with basic data types
- [ ] Service classes for common activities such as changing settings
## Usage
```bash
npm install @woocommerce/api --save-dev
```
Depending on what you're intending to get out of the API client there are a few different ways of using it.
### REST API
The simplest way to use the client is directly:
```javascript
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',
);
// 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',
);
// You can then use the client to make API requests.
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
// Access the status code from the response.
response.statusCode;
// Access the headers from the response.
response.headers;
// Access the data from the response, in this case, the products.
response.data;
} );
```
### Repositories
As a convenience utility we've created repositories for core data types that can simplify interacting with the API.
These repositories provide CRUD methods for ease-of-use:
```javascript
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 repository = SimpleProduct.restRepository( httpClient );
// The repository can now be used to create models.
const product = repository.create( { name: 'Simple Product', regularPrice: '9.99' } );
// The response will be one of the models with structured properties and TypeScript support.
product.id;
```

View File

@ -1,5 +1,6 @@
module.exports = {
preset: 'ts-jest',
rootDir: 'src',
testEnvironment: 'node',
testPathIgnorePatterns: [ '/node_modules/', '/dist/' ],
};

4933
tests/e2e/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
{
"name": "@woocommerce/model-factories",
"name": "@woocommerce/api",
"version": "0.1.0",
"author": "Automattic",
"description": "A simple interface for generating models of different types.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/factories/README.md",
"description": "A simple interface for interacting with a WooCommerce installation.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/api/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
@ -17,34 +17,33 @@
"types": "dist/index.d.ts",
"files": [
"/dist/",
"!*.ts.map",
"!*.tsbuildinfo",
"!*.spec.js",
"!*.spec.d.ts",
"!*.test.js",
"!*.test.d.ts"
"!/dist/**/__tests__/",
"!/dist/**/__mocks__/",
"!/dist/**/__snapshops__/"
],
"sideEffects": false,
"scripts": {
"test": "jest",
"clean": "rm -rf ./dist ./tsconfig.tsbuildinfo",
"compile": "tsc -b",
"build": "npm run clean && npm run compile",
"prepare": "npm run build"
"prepare": "npm run build",
"lint": "eslint src",
"test": "jest"
},
"dependencies": {
"axios": "0.19.2",
"create-hmac": "1.1.7",
"faker": "4.1.0",
"fishery": "1.0.0",
"oauth-1.0a": "2.2.6"
},
"devDependencies": {
"@types/create-hmac": "1.1.0",
"@types/faker": "4.1.12",
"@types/jest": "25.2.1",
"@types/moxios": "0.4.9",
"@types/moxios": "^0.4.9",
"@types/node": "13.13.5",
"jest": "25.5.4",
"jest-mock-extended": "^1.0.10",
"moxios": "0.4.0",
"ts-jest": "25.5.0",
"typescript": "3.8.3"

View File

@ -0,0 +1,76 @@
import { Model } from '../../models/model';
import { ModelRepository } from '../model-repository';
class DummyModel extends Model {
public name: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
describe( 'ModelRepository', () => {
it( 'should create', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( callback, null, null, null );
const created = await repository.create( { name: 'test' } );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { name: 'test' } );
} );
it( 'should throw error on create without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.create( { name: 'test' } ) ).toThrowError( /not supported/i );
} );
it( 'should read', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( null, callback, null, null );
const created = await repository.read( 1 );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( 1 );
} );
it( 'should throw error on read without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.read( 1 ) ).toThrowError( /not supported/i );
} );
it( 'should update', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( null, null, callback, null );
const updated = await repository.update( 1, { name: 'new-name' } );
expect( updated ).toBe( model );
expect( callback ).toHaveBeenCalledWith( 1, { name: 'new-name' } );
} );
it( 'should throw error on update without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.update( 1, { name: 'new-name' } ) ).toThrowError( /not supported/i );
} );
it( 'should delete', async () => {
const callback = jest.fn().mockResolvedValue( true );
const repository = new ModelRepository< DummyModel >( null, null, null, callback );
const success = await repository.delete( 1 );
expect( success ).toBe( true );
expect( callback ).toHaveBeenCalledWith( 1 );
} );
it( 'should throw error on delete without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.delete( 1 ) ).toThrowError( /not supported/i );
} );
} );

View File

@ -0,0 +1,197 @@
import { Model } from '../models/model';
/**
* A callback for creating a model using a data source.
*
* @callback CreateFn
* @param {Object} properties The properties of the model to create.
* @return {Promise.<Model>} Resolves to the created model.
*/
export type CreateFn< T > = ( properties: Partial< T > ) => Promise< T >;
/**
* A callback for reading a model using a data source.
*
* @callback ReadFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<Model>} Resolves to the read model.
*/
export type ReadFn< IDParam, T > = ( id: IDParam ) => Promise< T >;
/**
* A callback for updating a model using a data source.
*
* @callback UpdateFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<Model>} Resolves to the updated model.
*/
export type UpdateFn< IDParam, T > = ( id: IDParam, properties: Partial< T > ) => Promise< T >;
/**
* A callback for deleting a model from a data source.
*
* @callback DeleteFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<boolean>} Resolves to true once the model has been deleted.
*/
export type DeleteFn< IDParam > = ( id: IDParam ) => Promise< boolean >;
/**
* An interface for repositories that can create models.
*
* @typedef CreatesModels
* @property {CreateFn} create Creates a model using the repository.
*/
export interface CreatesModels< T extends Model > {
create( properties: Partial< T > ): Promise< T >;
}
/**
* An interface for repositories that can read models.
*
* @typedef ReadsModels
* @property {ReadFn} read Reads a model using the repository.
*/
export interface ReadsModels< T extends Model, IDParam = number > {
read( id: IDParam ): Promise< T >;
}
/**
* An interface for repositories that can update models.
*
* @typedef UpdatesModels
* @property {UpdateFn} update Updates a model using the repository.
*/
export interface UpdatesModels< T extends Model, IDParam = number > {
update( id: IDParam, properties: Partial< T > ): Promise< T >;
}
/**
* An interface for repositories that can delete models.
*
* @typedef DeletesModels
* @property {DeleteFn} delete Deletes a model using the repository.
*/
export interface DeletesModels< IDParam = number > {
delete( id: IDParam ): Promise< boolean >;
}
/**
* A class for performing CRUD operations on models using a number of internal hooks.
* Note that if a model does not support a given operation then it will throw an
* error when attempting to perform that action.
*/
export class ModelRepository< T extends Model, IDParam = number > implements
CreatesModels< T >,
ReadsModels< T, IDParam >,
UpdatesModels< T, IDParam >,
DeletesModels< IDParam > {
/**
* The hook used to create models
*
* @type {CreateFn}
* @private
*/
private readonly createHook: CreateFn< T > | null;
/**
* The hook used to read models.
*
* @type {ReadFn}
* @private
*/
private readonly readHook: ReadFn< IDParam, T > | null;
/**
* The hook used to update models.
*
* @type {UpdateFn}
* @private
*/
private readonly updateHook: UpdateFn< IDParam, T > | null;
/**
* The hook used to delete models.
*
* @type {DeleteFn}
* @private
*/
private readonly deleteHook: DeleteFn< IDParam > | null;
/**
* Creates a new repository instance.
*
* @param {CreateFn|null} createHook The hook for model creation.
* @param {ReadFn|null} readHook The hook for model reading.
* @param {UpdateFn|null} updateHook The hook for model updating.
* @param {DeleteFn|null} deleteHook The hook for model deletion.
*/
public constructor(
createHook: CreateFn< T > | null,
readHook: ReadFn< IDParam, T > | null,
updateHook: UpdateFn< IDParam, T > | null,
deleteHook: DeleteFn< IDParam > | null,
) {
this.createHook = createHook;
this.readHook = readHook;
this.updateHook = updateHook;
this.deleteHook = deleteHook;
}
/**
* Creates the given model.
*
* @param {Object} properties The properties for the model we'd like to create.
* @return {Promise.<Model>} A promise that resolves to the model after creation.
*/
public create( properties: Partial< T > ): Promise< T > {
if ( ! this.createHook ) {
throw new Error( 'The \'create\' operation is not supported on this model.' );
}
return this.createHook( properties );
}
/**
* Reads the given model.
*
* @param {number|Object} id The identifier for the model to read.
* @return {Promise.<Model>} A promise that resolves to the model.
*/
public read( id: IDParam ): Promise< T > {
if ( ! this.readHook ) {
throw new Error( 'The \'read\' operation is not supported on this model.' );
}
return this.readHook( id );
}
/**
* Updates the given model.
*
* @param {number|Object} id The identifier for the model to create.
* @param {Object} properties The model properties that we'd like to update.
* @return {Promise.<Model>} A promise that resolves to the model after updating.
*/
public update( id: IDParam, properties: Partial< T > ): Promise< T > {
if ( ! this.updateHook ) {
throw new Error( 'The \'update\' operation is not supported on this model.' );
}
return this.updateHook( id, properties );
}
/**
* Deletes the given model.
*
* @param {number|Object} id The identifier for the model to delete.
* @return {Promise.<boolean>} A promise that resolves to "true" on success.
*/
public delete( id: IDParam ): Promise< boolean > {
if ( ! this.deleteHook ) {
throw new Error( 'The \'delete\' operation is not supported on this model.' );
}
return this.deleteHook( id );
}
}

View File

@ -0,0 +1,56 @@
import * as moxios from 'moxios';
import { AxiosClient } from '../axios-client';
import { HTTPResponse } from '../../http-client';
import { AxiosInterceptor } from '../axios-interceptor';
import { mock } from 'jest-mock-extended';
describe( 'AxiosClient', () => {
let httpClient: AxiosClient;
beforeEach( () => {
moxios.install();
} );
afterEach( () => {
moxios.uninstall();
} );
it( 'should transform to HTTPResponse', async () => {
httpClient = new AxiosClient( { baseURL: 'http://test.test' } );
moxios.stubRequest( '/test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await httpClient.get( '/test' );
expect( response ).toBeInstanceOf( HTTPResponse );
expect( response ).toHaveProperty( 'statusCode', 200 );
expect( response ).toHaveProperty( 'headers', { 'content-type': 'application/json' } );
expect( response ).toHaveProperty( 'data', { test: 'value' } );
} );
it( 'should start extra interceptors', async () => {
const interceptor = mock< AxiosInterceptor >();
httpClient = new AxiosClient(
{ baseURL: 'http://test.test' },
[ interceptor ],
);
moxios.stubRequest( '/test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
await httpClient.get( '/test' );
expect( interceptor.start ).toHaveBeenCalled();
} );
} );

View File

@ -1,6 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import moxios from 'moxios';
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
import * as moxios from 'moxios';
import { AxiosOAuthInterceptor } from '../axios-oauth-interceptor';
describe( 'AxiosOAuthInterceptor', () => {
let apiAuthInterceptor: AxiosOAuthInterceptor;
@ -22,7 +22,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should not run unless started', async () => {
moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } );
moxios.stubRequest( 'https://api.test', { status: 200 } );
apiAuthInterceptor.stop( axiosInstance );
await axiosInstance.get( 'https://api.test' );
@ -38,7 +38,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should use basic auth for HTTPS', async () => {
moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } );
moxios.stubRequest( 'https://api.test', { status: 200 } );
await axiosInstance.get( 'https://api.test' );
const request = moxios.requests.mostRecent();
@ -51,7 +51,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should use OAuth 1.0a for HTTP', async () => {
moxios.stubOnce( 'GET', 'http://api.test', { status: 200 } );
moxios.stubRequest( 'http://api.test', { status: 200 } );
await axiosInstance.get( 'http://api.test' );
const request = moxios.requests.mostRecent();
@ -65,7 +65,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should work with base URL', async () => {
moxios.stubOnce( 'GET', '/test', { status: 200 } );
moxios.stubRequest( '/test', { status: 200 } );
await axiosInstance.request( {
method: 'GET',
baseURL: 'https://api.test/',

View File

@ -1,7 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import moxios from 'moxios';
import { APIResponse, APIError } from '../api-service';
import { AxiosResponseInterceptor } from './axios-response-interceptor';
import * as moxios from 'moxios';
import { AxiosResponseInterceptor } from '../axios-response-interceptor';
describe( 'AxiosResponseInterceptor', () => {
let apiResponseInterceptor: AxiosResponseInterceptor;
@ -19,8 +18,8 @@ describe( 'AxiosResponseInterceptor', () => {
moxios.uninstall();
} );
it( 'should transform responses into APIResponse', async () => {
moxios.stubOnce( 'GET', 'http://test.test', {
it( 'should transform responses into an HTTPResponse', async () => {
moxios.stubRequest( 'http://test.test', {
status: 200,
headers: {
'Content-Type': 'application/json',
@ -31,7 +30,7 @@ describe( 'AxiosResponseInterceptor', () => {
const response = await axiosInstance.get( 'http://test.test' );
expect( response ).toMatchObject( {
status: 200,
statusCode: 200,
headers: {
'content-type': 'application/json',
},
@ -41,21 +40,34 @@ describe( 'AxiosResponseInterceptor', () => {
} );
} );
it( 'should transform response errors into APIError', async () => {
moxios.stubOnce( 'GET', 'http://test.test', {
it( 'should transform error responses into an HTTPResponse', async () => {
moxios.stubRequest( 'http://test.test', {
status: 404,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { code: 'error_code', message: 'value', data: null } ),
responseText: JSON.stringify( { code: 'error_code', message: 'value' } ),
} );
const response = await axiosInstance.get( 'http://test.test' );
expect( response ).toMatchObject( {
statusCode: 404,
headers: {
'content-type': 'application/json',
},
data: {
code: 'error_code',
message: 'value',
},
} );
} );
it( 'should bubble non-response errors', async () => {
moxios.stubTimeout( 'http://test.test' );
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject(
new APIResponse(
404,
{ 'content-type': 'application/json' },
new APIError( 'error_code', 'value', null ),
),
new Error( 'timeout of 0ms exceeded' ),
);
} );
} );

View File

@ -0,0 +1,115 @@
import { HTTPClient, HTTPResponse } from '../http-client';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { AxiosInterceptor } from './axios-interceptor';
import { AxiosResponseInterceptor } from './axios-response-interceptor';
/**
* An HTTPClient implementation that uses Axios to make requests.
*/
export class AxiosClient implements HTTPClient {
/**
* An instance of the axios client for making HTTP requests.
*
* @type {AxiosInstance}
* @private
*/
private readonly client: AxiosInstance;
/**
* An array of interceptors that should be applied to the client.
*
* @type {AxiosInterceptor[]}
* @private
*/
private readonly interceptors: AxiosInterceptor[];
/**
* Creates a new axios client.
*
* @param {AxiosRequestConfig} config The request configuration.
* @param {AxiosInterceptor[]} extraInterceptors An array of additional interceptors to apply to the client.
*/
public constructor( config: AxiosRequestConfig, extraInterceptors: AxiosInterceptor[] = [] ) {
this.client = axios.create( config );
this.interceptors = extraInterceptors;
// The response interceptor needs to be last to prevent the other interceptors from
// receiving the transformed HTTPResponse type instead of an AxiosResponse.
this.interceptors.push( new AxiosResponseInterceptor() );
for ( const interceptor of this.interceptors ) {
interceptor.start( this.client );
}
}
/**
* Performs a GET request.
*
* @param {string} path The path we should send the request to.
* @param {Object} params Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public get< T = any >(
path: string,
params?: object,
): Promise< HTTPResponse< T >> {
return this.client.get( path, { params } );
}
/**
* Performs a POST request.
*
* @param {string} path The path we should send the request to.
* @param {Object} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public post< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.post( path, data );
}
/**
* Performs a PUT request.
*
* @param {string} path The path we should send the request to.
* @param {Object} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public put< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.put( path, data );
}
/**
* Performs a PATCH request.
*
* @param {string} path The path we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public patch< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.patch( path, data );
}
/**
* Performs a DELETE request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public delete< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.delete( path, { data } );
}
}

View File

@ -1,5 +1,13 @@
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
/**
* An object containing the IDs for an interceptor currently applied to a client.
*
* @typedef ActiveInterceptor
* @property {AxiosInstance} client The client the interceptor is tied to.
* @property {number} requestInterceptorID The ID of the request interceptor callbacks.
* @property {number} responseInterceptorID The ID of the response interceptor callbacks.
*/
type ActiveInterceptor = {
client: AxiosInstance;
requestInterceptorID: number;
@ -10,6 +18,12 @@ type ActiveInterceptor = {
* A base class for encapsulating the start and stop functionality required by all axios interceptors.
*/
export abstract class AxiosInterceptor {
/**
* An array of the active interceptor records for all of the clients this interceptor is attached to.
*
* @type {ActiveInterceptor[]}
* @private
*/
private readonly activeInterceptors: ActiveInterceptor[] = [];
/**

View File

@ -1,14 +1,26 @@
import { AxiosRequestConfig } from 'axios';
import createHmac from 'create-hmac';
import OAuth from 'oauth-1.0a';
import type { AxiosRequestConfig } from 'axios';
import * as createHmac from 'create-hmac';
import * as OAuth from 'oauth-1.0a';
import { AxiosInterceptor } from './axios-interceptor';
/**
* A utility class for managing the lifecycle of an authentication interceptor.
*/
export class AxiosOAuthInterceptor extends AxiosInterceptor {
/**
* The OAuth class for signing the request.
*
* @type {Object}
* @private
*/
private oauth: OAuth;
/**
* Creates a new interceptor.
*
* @param {string} consumerKey The consumer key of the API key.
* @param {string} consumerSecret The consumer secret of the API key.
*/
public constructor( consumerKey: string, consumerSecret: string ) {
super();

View File

@ -0,0 +1,34 @@
import { AxiosResponse } from 'axios';
import { AxiosInterceptor } from './axios-interceptor';
import { HTTPResponse } from '../http-client';
export class AxiosResponseInterceptor extends AxiosInterceptor {
/**
* Transforms the Axios response into our HTTP response.
*
* @param {AxiosResponse} response The response that we need to transform.
* @return {Promise} A promise containing the HTTPResponse.
*/
protected onResponseSuccess( response: AxiosResponse ): Promise< HTTPResponse > {
return Promise.resolve< HTTPResponse >(
new HTTPResponse( response.status, response.headers, response.data ),
);
}
/**
* Axios throws HTTP errors so we need to eat those errors and pass them normally.
*
* @param {*} error The error that was caught.
* @return {Promise} A promise containing the HTTPResponse.
*/
protected onResponseRejected( error: any ): Promise< HTTPResponse > {
// Convert HTTP response errors into a form that we can handle them with.
if ( error.response ) {
return Promise.resolve< HTTPResponse >(
new HTTPResponse( error.response.status, error.response.headers, error.response.data ),
);
}
throw error;
}
}

View File

@ -0,0 +1,2 @@
export { AxiosClient } from './axios-client';
export { AxiosOAuthInterceptor } from './axios-oauth-interceptor';

View File

@ -0,0 +1,39 @@
import { HTTPClient } from './http-client';
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
/**
* A class for generating HTTPClient instances with desired configurations.
*/
export class HTTPClientFactory {
/**
* Creates a new client instance prepared for basic auth.
*
* @param {string} apiURL
* @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 {
return new AxiosClient(
{
baseURL: apiURL,
auth: { username, password },
},
);
}
/**
* Creates a new client instance prepared for oauth.
*
* @param {string} apiURL
* @param {string} consumerKey
* @param {string} consumerSecret
* @return {HTTPClient} An HTTP client configured for OAuth requests.
*/
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
return new AxiosClient(
{ baseURL: apiURL },
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
);
}
}

View File

@ -0,0 +1,88 @@
/**
* A structured response from the HTTP client.
*/
export class HTTPResponse< T = any > {
/**
* The status code from the response.
*
* @type {number}
*/
public readonly statusCode: number;
/**
* The headers from the response.
*
* @type {Object.<string, string|string[]>}
*/
public readonly headers: any;
/**
* The data from the response.
*
* @type {Object}
*/
public readonly data: T;
/**
* Creates a new HTTP response instance.
*
* @param {number} statusCode The status code from the HTTP response.
* @param {Object.<string, string|string[]>} headers The headers from the HTTP response.
* @param {Object} data The data from the HTTP response.
*/
public constructor( statusCode: number, headers: any, data: T ) {
this.statusCode = statusCode;
this.headers = headers;
this.data = data;
}
}
/**
* An interface for clients that make HTTP requests.
*/
export interface HTTPClient {
/**
* Performs a GET request.
*
* @param {string} path The path we should send the request to.
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
get< T = any >( path: string, params?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a POST request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
post< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a PUT request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
put< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a PATCH request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
patch< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a DELETE request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
delete< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
}

View File

@ -0,0 +1,3 @@
export { HTTPResponse } from './http-client';
export type { HTTPClient } from './http-client';
export { HTTPClientFactory } from './http-client-factory';

View File

@ -0,0 +1,2 @@
export { HTTPClientFactory } from './http';
export * from './models';

View File

@ -0,0 +1 @@
export { SimpleProduct } from './products/simple-product';

View File

@ -0,0 +1,11 @@
/**
* A base class for all models.
*/
export abstract class Model {
/**
* The ID of the model if it exists.
*
* @type {number|null}
*/
public readonly id: number | null = null;
}

View File

@ -0,0 +1,20 @@
import { Model } from '../model';
/**
* The base class for all product types.
*/
export abstract class AbstractProduct extends Model {
/**
* The name of the product.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The regular price of the product when not discounted.
*
* @type {string}
*/
public readonly regularPrice: string = '';
}

View File

@ -0,0 +1,29 @@
import { AbstractProduct } from './abstract-product';
import { HTTPClient } from '../../http';
import { CreatesModels } from '../../framework/model-repository';
import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product';
/**
* A simple product object.
*/
export class SimpleProduct extends AbstractProduct {
/**
* Creates a new simple product instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< SimpleProduct > = {} ) {
super();
Object.assign( this, properties );
}
/**
* Creates a model repository configured for communicating via the REST API.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
* @return {CreatesModels} The created repository.
*/
public static restRepository( httpClient: HTTPClient ): CreatesModels< SimpleProduct > {
return simpleProductRESTRepository( httpClient );
}
}

View File

@ -0,0 +1,29 @@
import { simpleProductRESTRepository } from '../simple-product';
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { SimpleProduct } from '../../../../models';
import { CreatesModels } from '../../../../framework/model-repository';
describe( 'simpleProductRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: CreatesModels< SimpleProduct >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = simpleProductRESTRepository( httpClient );
} );
it( 'should create', async () => {
httpClient.post.mockResolvedValue( new HTTPResponse(
200,
{},
{ id: 123 },
) );
const created = await repository.create( { name: 'Test Product' } );
expect( created ).toBeInstanceOf( SimpleProduct );
expect( created ).toMatchObject( { id: 123 } );
expect( httpClient.post ).toHaveBeenCalledWith( '/wc/v3/products', { type: 'simple', name: 'Test Product' } );
} );
} );

View File

@ -0,0 +1,43 @@
import { HTTPClient } from '../../../http';
import { CreateFn, CreatesModels, ModelRepository } from '../../../framework/model-repository';
import { SimpleProduct } from '../../../models';
/**
* Creates a callback for REST model creation.
*
* @param {HTTPClient} httpClient The HTTP client for requests.
* @return {CreateFn} The callback for creating models via the REST API.
*/
function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProduct > {
return async ( properties ) => {
const response = await httpClient.post(
'/wc/v3/products',
{
type: 'simple',
name: properties.name,
regular_price: properties.regularPrice,
},
);
return Promise.resolve( new SimpleProduct( {
id: response.data.id,
name: response.data.name,
regularPrice: response.data.regular_price,
} ) );
};
}
/**
* Creates a new ModelRepository instance for interacting with models via the REST API.
*
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
* @return {CreatesModels} A repository for interacting with models via the REST API.
*/
export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesModels< SimpleProduct > {
return new ModelRepository(
restCreate( httpClient ),
null,
null,
null,
);
}

View File

@ -1,9 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"types": [ "node", "jest", "faker", "axios", "moxios", "create-hmac" ],
"types": [ "node", "jest", "axios", "moxios", "create-hmac" ],
"rootDir": "src",
"outDir": "dist"
"outDir": "dist",
"target": "es5"
},
"include": [ "src/" ]
}

216
tests/e2e/env/package-lock.json generated vendored
View File

@ -130,11 +130,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -1065,11 +1065,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -1308,9 +1308,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -1677,9 +1677,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -2584,9 +2584,9 @@
"integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw=="
},
"@types/babel__core": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz",
"integrity": "sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==",
"version": "7.1.10",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz",
"integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==",
"requires": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0",
@ -2604,9 +2604,9 @@
}
},
"@types/babel__template": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz",
"integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz",
"integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==",
"requires": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
@ -2675,9 +2675,9 @@
"integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM="
},
"@types/node": {
"version": "14.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.1.tgz",
"integrity": "sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw=="
"version": "14.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",
"integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@ -2705,9 +2705,9 @@
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
},
"@types/yargs": {
"version": "13.0.10",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz",
"integrity": "sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==",
"version": "13.0.11",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz",
"integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==",
"requires": {
"@types/yargs-parser": "*"
}
@ -2848,9 +2848,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -3047,9 +3047,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -3149,11 +3149,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"fill-range": {
@ -4231,12 +4231,12 @@
}
},
"browserslist": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.3.tgz",
"integrity": "sha512-GcZPC5+YqyPO4SFnz48/B0YaCwS47Q9iPChRGi6t7HhflKBcINzFrJvRfC+jp30sRMKxF+d4EHGs27Z0XP1NaQ==",
"version": "4.14.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz",
"integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==",
"requires": {
"caniuse-lite": "^1.0.30001131",
"electron-to-chromium": "^1.3.570",
"caniuse-lite": "^1.0.30001135",
"electron-to-chromium": "^1.3.571",
"escalade": "^3.1.0",
"node-releases": "^1.1.61"
}
@ -4317,9 +4317,9 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"caniuse-lite": {
"version": "1.0.30001131",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001131.tgz",
"integrity": "sha512-4QYi6Mal4MMfQMSqGIRPGbKIbZygeN83QsWq1ixpUwvtfgAZot5BrCKzGygvZaV+CnELdTwD0S4cqUNozq7/Cw=="
"version": "1.0.30001137",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001137.tgz",
"integrity": "sha512-54xKQZTqZrKVHmVz0+UvdZR6kQc7pJDgfhsMYDG19ID1BWoNnDMFm5Q3uSBSU401pBvKYMsHAt9qhEDcxmk8aw=="
},
"capture-exit": {
"version": "2.0.0",
@ -4346,12 +4346,12 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -4597,9 +4597,9 @@
}
},
"config": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/config/-/config-3.3.1.tgz",
"integrity": "sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/config/-/config-3.3.2.tgz",
"integrity": "sha512-NlGfBn2565YA44Irn7GV5KHlIGC3KJbf0062/zW5ddP9VXIuRj0m7HVyFAWvMZvaHPEglyGfwmevGz3KosIpCg==",
"requires": {
"json5": "^2.1.1"
}
@ -4971,9 +4971,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.570",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.570.tgz",
"integrity": "sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg=="
"version": "1.3.572",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.572.tgz",
"integrity": "sha512-TKqdEukCCl7JC20SwEoWTbtnGt4YjfHWAv4tcNky0a9qGo0WdM+Lrd60tps+nkaJCmktKBJjr99fLtEBU1ipWQ=="
},
"emoji-regex": {
"version": "8.0.0",
@ -5031,9 +5031,9 @@
}
},
"enzyme-adapter-react-16": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.4.tgz",
"integrity": "sha512-wPzxs+JaGDK2TPYzl5a9YWGce6i2SQ3Cg51ScLeyj2WotUZ8Obcq1ke/U1Y2VGpYlb9rrX2yCjzSMgtKCeAt5w==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz",
"integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==",
"requires": {
"enzyme-adapter-utils": "^1.13.1",
"enzyme-shallow-equal": "^1.0.4",
@ -5218,11 +5218,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"glob-parent": {
@ -5254,9 +5254,9 @@
}
},
"eslint-config-prettier": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz",
"integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz",
"integrity": "sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==",
"dev": true,
"requires": {
"get-stdin": "^6.0.0"
@ -5286,12 +5286,12 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -5339,9 +5339,9 @@
}
},
"eslint-plugin-react": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz",
"integrity": "sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg==",
"version": "7.21.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.2.tgz",
"integrity": "sha512-j3XKvrK3rpBzveKFbgAeGsWb9uz6iUOrR0jixRfjwdFeGSRsXvVTFtHDQYCjsd1/6Z/xvb8Vy3LiI5Reo7fDrg==",
"dev": true,
"requires": {
"array-includes": "^3.1.1",
@ -6228,11 +6228,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -6475,9 +6475,9 @@
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-callable": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz",
"integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg=="
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
},
"is-ci": {
"version": "2.0.0",
@ -6774,11 +6774,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -6968,9 +6968,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -7115,11 +7115,11 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"detect-newline": {
@ -9133,12 +9133,12 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"fill-range": {
@ -9217,9 +9217,9 @@
}
},
"nearley": {
"version": "2.19.6",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.6.tgz",
"integrity": "sha512-OV3Lx+o5iIGWVY38zs+7aiSnBqaHTFAOQiz83VHJje/wOOaSgzE3H0S/xfISxJhFSoPcX611OEDV9sCT8F283g==",
"version": "2.19.7",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.7.tgz",
"integrity": "sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==",
"requires": {
"commander": "^2.19.0",
"moo": "^0.5.0",
@ -9949,11 +9949,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -9997,12 +9997,12 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"https-proxy-agent": {
@ -11684,11 +11684,11 @@
"integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {

View File

@ -1,58 +0,0 @@
# Model Factories
A simple interface for generating models of different types.
## Installation
``bash
npm install @woocommerce/model-factories --save-dev
``
## Usage
Consumers of this package should rely on an instance of `ModelRegistry` to access the factories.
Here is an example of how to initialize and use the package to generate a simple product:
```javascript
import {
AdapterTypes,
initializeUsingBasicAuth,
ModelRegistry,
registerSimpleProduct,
SimpleProduct
} from '@woocommerce/model-factories';
// The ModelRegistry instance is where all of the factories and adapters are stored in an easy-to-access way.
const modelRegistry = new ModelRegistry()
// Call the register functions to add a kind of factory to the model registry.
// This will also add any adapters we've created for the factory, allowing it
// to be created on the server.
registerSimpleProduct( modelRegistry );
// Before you can use the included API adapter you need to initialize it using one of the utility methods.
// If you do not initialize the API adapters they will not be able to make requests to the API.
// Note that these utility functions only set up adapters that have been registered already
// and so further calls to `registeryXXX` functions will have adapters that aren't ready.
initializeUsingBasicAuth( modelRegistry, 'https://test.test/wp-json', 'admin', 'password' );
initializeUsingOAuth( modelRegistry, 'https://test.test/wp-json', 'consumer_key', 'consumer_secret' );
// In order to actually create the models on the server, each registered factory must have an adapter set.
// You can do this on a per-factory basis using
modelRegistry.changeFactoryAdapter( SimpleProduct, AdapterTypes.API );
// You can do this to all factories registered using
modelRegistry.changeAllFactoryAdapters( AdapterTypes.API );
// Once all of the initialization has been taken care of you can create models!
// Any fields that are not defined will be filled out by random data.
const product = await modelRegistry.getFactory( SimpleProduct ).create( { name: 'Test Product' } );
// You can now access the ID of the created model using `product.id`!
// You can also create models in bulk!
const poducts = await modelRegistry.getFactory( SimpleProduct ).createList( 5 );
// You now have an array of products to work with!
```
## Custom Models
## Custom Adapters

View File

@ -1,16 +0,0 @@
import { Model } from './model';
/**
* An interface for implementing adapters to create models.
*/
export interface Adapter<T extends Model> {
/**
* Creates a model or array of models using a service..
*
* @param {Model|Model[]} model The model or array of models to create.
* @return {Promise} Resolves to the created input model or array of models.
*/
create( model: T ): Promise<T>;
create( model: T[] ): Promise<T[]>;
create( model: T | T[] ): Promise<T> | Promise<T[]>;
}

View File

@ -1,55 +0,0 @@
import { Model } from '../model';
import { APIAdapter } from './api-adapter';
import { SimpleProduct } from '../../models/simple-product';
import { APIResponse, APIService } from './api-service';
class MockAPI implements APIService {
public get = jest.fn();
public post = jest.fn();
public put = jest.fn();
public patch = jest.fn();
public delete = jest.fn();
}
describe( 'APIModelCreator', () => {
let adapter: APIAdapter<Model>;
let mockService: MockAPI;
beforeEach( () => {
adapter = new APIAdapter( '/wc/v3/product', () => 'test' );
mockService = new MockAPI();
adapter.setAPIService( mockService );
} );
it( 'should create single instance', async () => {
mockService.post.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) );
const result = await adapter.create( new SimpleProduct() );
expect( result ).toBeInstanceOf( SimpleProduct );
expect( result.id ).toBe( 1 );
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
} );
it( 'should create multiple instances', async () => {
mockService.post
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) )
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 2 } ) ) )
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 3 } ) ) );
const result = await adapter.create( [ new SimpleProduct(), new SimpleProduct(), new SimpleProduct() ] );
expect( result ).toBeInstanceOf( Array );
expect( result ).toHaveLength( 3 );
expect( result[ 0 ].id ).toBe( 1 );
expect( result[ 1 ].id ).toBe( 2 );
expect( result[ 2 ].id ).toBe( 3 );
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
expect( mockService.post.mock.calls[ 1 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 1 ][ 1 ] ).toBe( 'test' );
expect( mockService.post.mock.calls[ 2 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 2 ][ 1 ] ).toBe( 'test' );
} );
} );

View File

@ -1,87 +0,0 @@
import { APIResponse, APIService } from './api-service';
import { Model } from '../model';
import { Adapter } from '../adapter';
/**
* A callback for transforming models into an API request body.
*
* @callback APITransformerFn
* @param {Model} model The model that we want to transform.
* @return {*} The structured request data for the API.
*/
export type APITransformerFn<T extends Model> = ( model: T ) => any;
/**
* A class used for creating data models using a supplied API endpoint.
*/
export class APIAdapter<T extends Model> implements Adapter<T> {
private readonly endpoint: string;
private readonly transformer: APITransformerFn<T>;
private apiService: APIService | null;
public constructor( endpoint: string, transformer: APITransformerFn<T> ) {
this.endpoint = endpoint;
this.transformer = transformer;
this.apiService = null;
}
/**
* Sets the API service that the adapter should use for creation actions.
*
* @param {APIService|null} service The new API service for the adapter to use.
*/
public setAPIService( service: APIService | null ): void {
this.apiService = service;
}
/**
* Creates a model or array of models using the API service.
*
* @param {Model|Model[]} model The model or array of models to create.
* @return {Promise} Resolves to the created input model or array of models.
*/
public create( model: T ): Promise<T>;
public create( model: T[] ): Promise<T[]>;
public create( model: T | T[] ): Promise<T> | Promise<T[]> {
if ( ! this.apiService ) {
throw new Error( 'An API service must be registered for the adapter to work.' );
}
if ( Array.isArray( model ) ) {
return this.createList( model );
}
return this.createSingle( model );
}
/**
* Creates a single model using the API service.
*
* @param {Model} model The model to create.
* @return {Promise} Resolves to the created input model.
*/
private async createSingle( model: T ): Promise<T> {
return this.apiService!.post(
this.endpoint,
this.transformer( model ),
).then( ( data: APIResponse ) => {
model.setID( data.data.id );
return model;
} );
}
/**
* Creates an array of models using the API service.
*
* @param {Model[]} models The array of models to create.
* @return {Promise} Resolves to the array of created input models.
*/
private async createList( models: T[] ): Promise<T[]> {
const promises: Promise<T>[] = [];
for ( const model of models ) {
promises.push( this.createSingle( model ) );
}
return Promise.all( promises );
}
}

View File

@ -1,100 +0,0 @@
/**
* A structured response from the API.
*/
export class APIResponse<T = any> {
public readonly status: number;
public readonly headers: any;
public readonly data: T;
public constructor( status: number, headers: any, data: T ) {
this.status = status;
this.headers = headers;
this.data = data;
}
}
/**
* A structured error from the API.
*/
export class APIError {
public readonly code: string;
public readonly message: string;
public readonly data: any;
public constructor( code: string, message: string, data: any ) {
this.code = code;
this.message = message;
this.data = data;
}
}
/**
* Checks whether or not an APIResponse contains an error.
*
* @param {APIResponse} response The response to evaluate.
*/
export function isAPIError( response: APIResponse ): response is APIResponse<APIError> {
return response.status < 200 || response.status >= 400;
}
/**
* An interface for implementing services to make calls against the API.
*/
export interface APIService {
/**
* Performs a GET request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
get<T>(
endpoint: string,
params?: any
): Promise<APIResponse<T>>;
/**
* Performs a POST request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
post<T>(
endpoint: string,
data?: any
): Promise<APIResponse<T>>;
/**
* Performs a PUT request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
put<T>( endpoint: string, data?: any ): Promise<APIResponse<T>>;
/**
* Performs a PATCH request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
patch<T>(
endpoint: string,
data?: any
): Promise<APIResponse<T>>;
/**
* Performs a DELETE request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
delete<T>(
endpoint: string,
data?: any
): Promise<APIResponse<T>>;
}

View File

@ -1,57 +0,0 @@
import moxios from 'moxios';
import { APIResponse } from '../api-service';
import { AxiosAPIService } from './axios-api-service';
describe( 'AxiosAPIService', () => {
let apiClient: AxiosAPIService;
beforeEach( () => {
moxios.install();
} );
afterEach( () => {
moxios.uninstall();
} );
it( 'should add OAuth interceptors', async () => {
apiClient = AxiosAPIService.createUsingOAuth(
'http://test.test/wp-json/',
'consumer_key',
'consumer_secret',
);
moxios.stubOnce( 'GET', '/wc/v2/product', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await apiClient.get( '/wc/v2/product' );
expect( response ).toBeInstanceOf( APIResponse );
const request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toMatch( /^OAuth/ );
} );
it( 'should add basic auth interceptors', async () => {
apiClient = AxiosAPIService.createUsingBasicAuth( 'http://test.test/wp-json/', 'test', 'pass' );
moxios.stubOnce( 'GET', '/wc/v2/product', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await apiClient.get( '/wc/v2/product' );
expect( response ).toBeInstanceOf( APIResponse );
const request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toMatch( /^Basic/ );
} );
} );

View File

@ -1,127 +0,0 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { APIResponse, APIService } from '../api-service';
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
import { AxiosInterceptor } from './axios-interceptor';
import { AxiosResponseInterceptor } from './axios-response-interceptor';
/**
* An API service implementation that uses Axios to make requests to the WordPress API.
*/
export class AxiosAPIService implements APIService {
private readonly client: AxiosInstance;
private readonly interceptors: AxiosInterceptor[];
public constructor( config: AxiosRequestConfig, interceptors: AxiosInterceptor[] = [] ) {
this.client = axios.create( config );
this.interceptors = interceptors;
for ( const interceptor of this.interceptors ) {
interceptor.start( this.client );
}
}
/**
* Creates a new Axios API Service using OAuth 1.0a one-legged authentication.
*
* @param {string} apiURL The base URL for the API requests to be sent.
* @param {string} consumerKey The OAuth consumer key.
* @param {string} consumerSecret The OAuth consumer secret.
* @return {AxiosAPIService} The created service.
*/
public static createUsingOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): AxiosAPIService {
return new AxiosAPIService(
{ baseURL: apiURL },
[
new AxiosOAuthInterceptor( consumerKey, consumerSecret ),
new AxiosResponseInterceptor(),
],
);
}
/**
* Creates a new Axios API Service using basic authentication.
*
* @param {string} apiURL The base URL for the API requests to be sent.
* @param {string} username The username for authentication.
* @param {string} password The password for authentication.
* @return {AxiosAPIService} The created service.
*/
public static createUsingBasicAuth( apiURL: string, username: string, password: string ): AxiosAPIService {
return new AxiosAPIService(
{
baseURL: apiURL,
auth: { username, password },
},
[ new AxiosResponseInterceptor() ],
);
}
/**
* Performs a GET request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public get<T>(
endpoint: string,
params?: any,
): Promise<APIResponse<T>> {
return this.client.get( endpoint, { params } );
}
/**
* Performs a POST request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public post<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.post( endpoint, data );
}
/**
* Performs a PUT request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public put<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.put( endpoint, data );
}
/**
* Performs a PATCH request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public patch<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.patch( endpoint, data );
}
/**
* Performs a DELETE request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public delete<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.delete( endpoint, { data } );
}
}

View File

@ -1,39 +0,0 @@
import { AxiosResponse } from 'axios';
import { APIResponse, APIError } from '../api-service';
import { AxiosInterceptor } from './axios-interceptor';
export class AxiosResponseInterceptor extends AxiosInterceptor {
/**
* Transforms the Axios response into our API response to be consumed in a consistent manner.
*
* @param {AxiosResponse} response The respons ethat we need to transform.
* @return {Promise} A promise containing the APIResponse.
*/
protected onResponseSuccess( response: AxiosResponse ): Promise<APIResponse> {
return Promise.resolve<APIResponse>(
new APIResponse( response.status, response.headers, response.data ),
);
}
/**
* Transforms HTTP errors into an API error if the error came from the API.
*
* @param {*} error The error that was caught.
*/
protected onResponseRejected( error: any ): Promise<APIResponse> {
// Only transform API errors.
if ( ! error.response ) {
throw error;
}
throw new APIResponse(
error.response.status,
error.response.headers,
new APIError(
error.response.data.code,
error.response.data.message,
error.response.data.data,
),
);
}
}

View File

@ -1,14 +0,0 @@
/**
* CORE CLASSES
* These exports relate to extending the core functionality of the package.
*/
export { Adapter } from './adapter';
export { ModelFactory } from './model-factory';
export { Model } from './model';
/**
* API ADAPTER
* These exports relate to replacing the underlying HTTP layer of API adapters.
*/
export { APIAdapter } from './api/api-adapter';
export { APIService, APIResponse, APIError } from './api/api-service';

View File

@ -1,41 +0,0 @@
import { ModelFactory } from './model-factory';
import { Adapter } from './adapter';
import { Product } from '../models/product';
import { SimpleProduct } from '../models/simple-product';
class MockAdapter implements Adapter<Product> {
public create = jest.fn();
}
describe( 'ModelFactory', () => {
let mockAdapter: MockAdapter;
let factory: ModelFactory<Product>;
beforeEach( () => {
mockAdapter = new MockAdapter();
factory = ModelFactory.define<Product, any, ModelFactory<Product>>(
( { params } ) => {
return new SimpleProduct( params );
},
);
} );
it( 'should error without adapter', async () => {
expect( () => factory.create() ).toThrowError( /no adapter/ );
} );
it( 'should create using adapter', async () => {
factory.setAdapter( mockAdapter );
const expectedModel = new SimpleProduct( { name: 'test2' } );
expectedModel.setID( 1 );
mockAdapter.create.mockReturnValueOnce( Promise.resolve( expectedModel ) );
const created = await factory.create( { name: 'test' } );
expect( mockAdapter.create.mock.calls ).toHaveLength( 1 );
expect( created ).toBeInstanceOf( Product );
expect( created.id ).toBe( 1 );
expect( created.name ).toBe( 'test2' );
} );
} );

View File

@ -1,52 +0,0 @@
import { DeepPartial, Factory, BuildOptions } from 'fishery';
import { Model } from './model';
import { Adapter } from './adapter';
/**
* A factory that can be used to create models using an adapter.
*/
export class ModelFactory<T extends Model, I = any> extends Factory<T, I> {
private adapter: Adapter<T> | null = null;
/**
* Sets the adapter that the factory will use to create models.
*
* @param {Adapter|null} adapter
*/
public setAdapter( adapter: Adapter<T> | null ): void {
this.adapter = adapter;
}
/**
* Create an object using your factory
*
* @param {DeepPartial} params The parameters that should populate the object.
* @param {BuildOptions} options The options to be used in the builder.
* @return {Promise} Resolves to the created model.
*/
public create( params?: DeepPartial<T>, options?: BuildOptions<T, I> ): Promise<T> {
if ( ! this.adapter ) {
throw new Error( 'The factory has no adapter to create using.' );
}
const model = this.build( params, options );
return this.adapter.create( model );
}
/**
* Create an array of objects using your factory
*
* @param {number} number The number of models to create.
* @param {DeepPartial} params The parameters that should populate the object.
* @param {BuildOptions} options The options to be used in the builder.
* @return {Promise} Resolves to the created model.
*/
public createList( number: number, params?: DeepPartial<T>, options?: BuildOptions<T, I> ): Promise<T[]> {
if ( ! this.adapter ) {
throw new Error( 'The factory has no adapter to create using.' );
}
const model = this.buildList( number, params, options );
return this.adapter.create( model );
}
}

View File

@ -1,45 +0,0 @@
import { AdapterTypes, ModelRegistry } from './model-registry';
import { ModelFactory } from './model-factory';
import { Product } from '../models/product';
import { APIAdapter } from './api/api-adapter';
import { SimpleProduct } from '../models/simple-product';
describe( 'ModelRegistry', () => {
let factoryRegistry: ModelRegistry;
beforeEach( () => {
factoryRegistry = new ModelRegistry();
} );
it( 'should register factories once', () => {
const factory = ModelFactory.define<Product, any, ModelFactory<Product>>( ( { params } ) => {
return new SimpleProduct( params );
} );
expect( factoryRegistry.getFactory( SimpleProduct ) ).toBeNull();
factoryRegistry.registerFactory( SimpleProduct, factory );
expect( () => factoryRegistry.registerFactory( SimpleProduct, factory ) )
.toThrowError( /already been registered/ );
const loaded = factoryRegistry.getFactory( SimpleProduct );
expect( loaded ).toBe( factory );
} );
it( 'should register adapters once', () => {
const adapter = new APIAdapter<Product>( '', ( model ) => model );
expect( factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API ) ).toBeNull();
factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter );
expect( () => factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter ) )
.toThrowError( /already been registered/ );
const loaded = factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API );
expect( loaded ).toBe( adapter );
} );
} );

View File

@ -1,125 +0,0 @@
import { Adapter } from './adapter';
import { Model } from './model';
import { ModelFactory } from './model-factory';
type Registry<T> = { [key: string ]: T };
/**
* The types of adapters that can be stored in the registry.
*
* @typedef AdapterTypes
* @property {string} API "api"
* @property {string} Custom "custom"
*/
export enum AdapterTypes {
API = 'api',
Custom = 'custom'
}
/**
* A registry that allows for us to easily manage all of our factories and related state.
*/
export class ModelRegistry {
private readonly factories: Registry<ModelFactory<any>> = {};
private readonly adapters: { [key in AdapterTypes]: Registry<Adapter<any>> } = {
api: {},
custom: {},
};
/**
* Registers a factory for the class.
*
* @param {Function} modelClass The class of model we're registering the factory for.
* @param {ModelFactory} factory The factory that we're registering.
*/
public registerFactory<T extends Model>( modelClass: new () => T, factory: ModelFactory<T> ): void {
if ( this.factories.hasOwnProperty( modelClass.name ) ) {
throw new Error( 'A factory of this type has already been registered for the model class.' );
}
this.factories[ modelClass.name ] = factory;
}
/**
* Fetches a factory that was registered for the class.
*
* @param {Function} modelClass The class of model for the factory we're fetching.
*/
public getFactory<T extends Model>( modelClass: new () => T ): ModelFactory<T> | null {
if ( this.factories.hasOwnProperty( modelClass.name ) ) {
return this.factories[ modelClass.name ];
}
return null;
}
/**
* Registers an adapter for the class.
*
* @param {Function} modelClass The class of model that we're registering the adapter for.
* @param {AdapterTypes} type The type of adapter that we're registering.
* @param {Adapter} adapter The adapter that we're registering.
*/
public registerAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes, adapter: Adapter<T> ): void {
if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) {
throw new Error( 'An adapter of this type has already been registered for the model class.' );
}
this.adapters[ type ][ modelClass.name ] = adapter;
}
/**
* Fetches an adapter registered for the class.
*
* @param {Function} modelClass The class of the model for the adapter we're fetching.
* @param {AdapterTypes} type The type of adapter we're fetching.
*/
public getAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes ): Adapter<T> | null {
if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) {
return this.adapters[ type ][ modelClass.name ];
}
return null;
}
/**
* Fetches all of the adapters of a given type from the registry.
*
* @param {AdapterTypes} type The type of adapters to fetch.
*/
public getAdapters( type: AdapterTypes ): Adapter<any>[] {
return Object.values( this.adapters[ type ] );
}
/**
* Changes the adapter a factory is using.
*
* @param {Function} modelClass The class of the model factory we're changing.
* @param {AdapterTypes} type The type of adapter to set.
*/
public changeFactoryAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes ): void {
const factory = this.getFactory( modelClass );
if ( ! factory ) {
throw new Error( 'No factory defined for this model class.' );
}
const adapter = this.getAdapter( modelClass, type );
if ( ! adapter ) {
throw new Error( 'No adapter of this type registered for this model class.' );
}
factory.setAdapter( adapter );
}
/**
* Changes the adapters of all factories to the given type or null if one is not registered for that type.
*
* @param {AdapterTypes} type The type of adapter to set.
*/
public changeAllFactoryAdapters( type: AdapterTypes ): void {
for ( const key in this.factories ) {
this.factories[ key ].setAdapter(
this.adapters[ type ][ key ] || null,
);
}
}
}

View File

@ -1,20 +0,0 @@
import { DeepPartial } from 'fishery';
/**
* A base class for all models.
*/
export abstract class Model {
private _id: number = 0;
protected constructor( partial: DeepPartial<any> = {} ) {
Object.assign( this, partial );
}
public get id(): number {
return this._id;
}
public setID( id: number ): void {
this._id = id;
}
}

View File

@ -1,17 +0,0 @@
/**
* FRAMEWORK CLASSES
* These exports relate to the core classes needed to utilize the package.
*/
export { ModelRegistry, AdapterTypes } from './framework/model-registry';
/**
* MODELS
* This exports all of the models we have defined and their related functions.
*/
export * from './models';
/**
* UTILITIES
* These exports relate to common utilities that can be used to utilize the package.
*/
export { initializeUsingOAuth, initializeUsingBasicAuth } from './utils';

View File

@ -1,2 +0,0 @@
export { Product } from './product';
export { SimpleProduct, registerSimpleProduct } from './simple-product';

View File

@ -1,15 +0,0 @@
import { Model } from '../framework/model';
import { DeepPartial } from 'fishery';
/**
* The base class for all product types.
*/
export abstract class Product extends Model {
public readonly name: string = '';
public readonly regularPrice: string = '';
protected constructor( partial: DeepPartial<Product> = {} ) {
super( partial );
Object.assign( this, partial );
}
}

View File

@ -1,48 +0,0 @@
import { DeepPartial } from 'fishery';
import { Product } from './product';
import { AdapterTypes, ModelRegistry } from '../framework/model-registry';
import { ModelFactory } from '../framework/model-factory';
import { APIAdapter } from '../framework/api/api-adapter';
import faker from 'faker/locale/en';
export class SimpleProduct extends Product {
public constructor( partial: DeepPartial<SimpleProduct> = {} ) {
super( partial );
Object.assign( this, partial );
}
}
/**
* Registers the simple product factory and adapters.
*
* @param {ModelRegistry} registry The registry to hold the model reference.
*/
export function registerSimpleProduct( registry: ModelRegistry ): void {
if ( null !== registry.getFactory( SimpleProduct ) ) {
return;
}
const factory = ModelFactory.define<SimpleProduct, any, ModelFactory<SimpleProduct>>(
( { params } ) => {
return new SimpleProduct(
{
name: params.name ?? faker.commerce.productName(),
regularPrice: params.regularPrice ?? faker.commerce.price(),
},
);
},
);
registry.registerFactory( SimpleProduct, factory );
const apiAdapter = new APIAdapter<SimpleProduct>(
'/wc/v3/products',
( model ) => {
return {
type: 'simple',
name: model.name,
regular_price: model.regularPrice,
};
},
);
registry.registerAdapter( SimpleProduct, AdapterTypes.API, apiAdapter );
}

View File

@ -1,55 +0,0 @@
import { AdapterTypes, ModelRegistry } from './framework/model-registry';
import { APIAdapter } from './framework/api/api-adapter';
import { AxiosAPIService } from './framework/api/axios/axios-api-service';
/**
* Initializes all of the APIAdapters with a client to communicate with the API.
*
* @param {ModelRegistry} registry The model registry that we want to initialize.
* @param {string} apiURL The base URL for the API.
* @param {string} consumerKey The OAuth consumer key for the API service.
* @param {string} consumerSecret The OAuth consumer secret for the API service.
*/
export function initializeUsingOAuth(
registry: ModelRegistry,
apiURL: string,
consumerKey: string,
consumerSecret: string,
): void {
const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter<any>[];
if ( ! adapters.length ) {
return;
}
const apiService = AxiosAPIService.createUsingOAuth( apiURL, consumerKey, consumerSecret );
for ( const adapter of adapters ) {
adapter.setAPIService( apiService );
}
}
/**
* Initialize all of the APIAdapters with a client to communicate with the API.
*
*
*
* @param {ModelRegistry} registry The model registry that we want to initialize.
* @param {string} apiURL The base URL for the API.
* @param {string} username The username to use for authentication.
* @param {string} password The password to use for authentication.
*/
export function initializeUsingBasicAuth(
registry: ModelRegistry,
apiURL: string,
username: string,
password: string,
): void {
const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter<any>[];
if ( ! adapters.length ) {
return;
}
const apiService = AxiosAPIService.createUsingBasicAuth( apiURL, username, password );
for ( const adapter of adapters ) {
adapter.setAPIService( apiService );
}
}

View File

@ -107,6 +107,19 @@
"iconv-lite": "^0.6.2"
}
},
"faker": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/faker/-/faker-5.1.0.tgz",
"integrity": "sha512-RrWKFSSA/aNLP0g3o2WW1Zez7/MnMr7xkiZmoCfAGZmdkDQZ6l2KtuXHN5XjdvpRjDl8+3vf+Rrtl06Z352+Mw=="
},
"fishery": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fishery/-/fishery-1.0.1.tgz",
"integrity": "sha512-VV8H4ZuCbZ9cCWkrYWLLPoAfpTp0t+hlJVoNWkRRHdXOgQ08wjd8ab9di8/Ed/QhgwxY3h7Y17HgDZ9osaHSSQ==",
"requires": {
"lodash.mergewith": "^4.6.2"
}
},
"gettext-parser": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz",
@ -139,6 +152,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"memize": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz",

View File

@ -11,8 +11,10 @@
"main": "build/index.js",
"module": "build-module/index.js",
"dependencies": {
"@woocommerce/api": "file:../api",
"@wordpress/e2e-test-utils": "4.6.0",
"@woocommerce/model-factories": "file:../factories"
"faker": "^5.1.0",
"fishery": "^1.0.1"
},
"publishConfig": {
"access": "public"

View File

@ -6,9 +6,8 @@
* Internal dependencies
*/
import { StoreOwnerFlow } from './flows';
import modelRegistry from './factories';
import { SimpleProduct } from '@woocommerce/model-factories';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './page-utils';
import factories from './factories';
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );
@ -77,10 +76,10 @@ const completeOnboardingWizard = async () => {
// Query for the industries checkboxes
const industryCheckboxes = await page.$$( '.components-checkbox-control__input' );
expect( industryCheckboxes ).toHaveLength( 9 );
expect( industryCheckboxes ).toHaveLength( 8 );
// Select all industries including "Other"
for ( let i = 0; i < 9; i++ ) {
for ( let i = 0; i < 8; i++ ) {
await industryCheckboxes[i].click();
}
@ -172,7 +171,7 @@ const completeOnboardingWizard = async () => {
// Benefits section
// Wait for Benefits section to appear
await page.waitForSelector( '.woocommerce-profile-wizard__step-header' );
await page.waitForSelector( '.woocommerce-profile-wizard__benefits' );
// Wait for "No thanks" button to become active
await page.waitForSelector( 'button.is-secondary:not(:disabled)' );
@ -346,7 +345,7 @@ const completeOldSetupWizard = async () => {
* Create simple product.
*/
const createSimpleProduct = async () => {
const product = await modelRegistry.getFactory( SimpleProduct ).create( {
const product = await factories.products.simple.create( {
name: simpleProductName,
regularPrice: '9.99'
} );

View File

@ -1,24 +1,18 @@
import {
AdapterTypes,
initializeUsingBasicAuth,
ModelRegistry,
registerSimpleProduct,
} from '@woocommerce/model-factories';
import { HTTPClientFactory } from '@woocommerce/api';
const config = require( 'config' );
const modelRegistry = new ModelRegistry()
// Register all of the different factories that we're going to need.
registerSimpleProduct( modelRegistry );
// Make sure to perform the initialization AFTER registering all of the factories, otherwise the adapters might be
// missed on subsequent registrations.
initializeUsingBasicAuth( modelRegistry,
const httpClient = HTTPClientFactory.withBasicAuth(
config.get( 'url' ) + '/wp-json',
config.get( 'users.admin.username' ),
config.get( 'users.admin.password' )
config.get( 'users.admin.password' ),
);
modelRegistry.changeAllFactoryAdapters( AdapterTypes.API );
export default modelRegistry;
import { simpleProductFactory } from './factories/simple-product';
const factories = {
products: {
simple: simpleProductFactory( httpClient ),
},
};
export default factories;

View File

@ -0,0 +1,41 @@
import { Factory } from 'fishery';
/**
* A temporary class until Fishery includes better async support.
*/
export class AsyncFactory extends Factory {
constructor( generator, creator ) {
super( generator );
this.creator = creator;
}
/**
* Create an object using your factory
*
* @param {*} params The parameters that should populate the object.
* @param {*} options The options to be used in the builder.
* @return {Promise} Resolves to the created model.
*/
create( params = {}, options = {} ) {
const model = this.build( params, options );
return this.creator( model );
}
/**
* Create an array of objects using your factory
*
* @param {number} number The number of models to create.
* @param {*} params The parameters that should populate the object.
* @param {*} options The options to be used in the builder.
* @return {Promise} Resolves to the created models.
*/
createList( number, params = {}, options = {} ) {
const models = this.buildList( number, params, options );
const promises = [];
for ( const model of models ) {
promises.push( this.creator( model ) );
}
return Promise.all( promises );
}
}

View File

@ -0,0 +1,23 @@
import { SimpleProduct } from '@woocommerce/api';
import { AsyncFactory } from './async-factory';
import faker from 'faker/locale/en';
/**
* Creates a new factory for creating models.
*
* @param {HTTPClient} httpClient The HTTP client we will give the repository.
* @return {AsyncFactory} The factory for creating models.
*/
export function simpleProductFactory( httpClient ) {
const repository = SimpleProduct.restRepository( httpClient );
return new AsyncFactory(
( { params } ) => {
return new SimpleProduct( {
name: params.name ?? faker.commerce.productName(),
regularPrice: params.regularPrice ?? faker.commerce.price(),
} );
},
( params ) => repository.create( params ),
);
}

View File

@ -1,20 +1,32 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"incremental": true,
"allowJs": true,
"checkJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"target": "es6",
"module": "commonjs",
"incremental": true,
"declaration": true,
"declarationMap": true,
"composite": true,
"emitDeclarationOnly": false,
"isolatedModules": true,
/* Strict Type-Checking Options */
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
/* Module Resolution Options */
"moduleResolution": "node",
/* This needs to be false so our types are possible to consume without setting this */
"esModuleInterop": false,
"resolveJsonModule": true
}
}

View File

@ -1,6 +1,6 @@
{
"references": [
{ "path": "tests/e2e/factories" }
{ "path": "tests/e2e/api" }
],
"files": []
}