Merge pull request #27836 from woocommerce/packages/api/add/repository-listing

@woocommerce/api: Added list() repository operation and classes for managing store settings
This commit is contained in:
Christopher Allford 2020-10-08 13:20:12 -07:00 committed by GitHub
commit f9ef91adc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 25909 additions and 9536 deletions

24304
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,9 +37,9 @@
"@babel/polyfill": "7.10.4", "@babel/polyfill": "7.10.4",
"@babel/preset-env": "7.10.4", "@babel/preset-env": "7.10.4",
"@babel/register": "7.10.4", "@babel/register": "7.10.4",
"@typescript-eslint/eslint-plugin": "3.1.0", "@typescript-eslint/eslint-plugin": "3.10.1",
"@typescript-eslint/experimental-utils": "^2.34.0", "@typescript-eslint/experimental-utils": "3.10.1",
"@typescript-eslint/parser": "3.1.0", "@typescript-eslint/parser": "3.10.1",
"@woocommerce/api": "file:tests/e2e/api", "@woocommerce/api": "file:tests/e2e/api",
"@woocommerce/e2e-core-tests": "file:tests/e2e/core-tests", "@woocommerce/e2e-core-tests": "file:tests/e2e/core-tests",
"@woocommerce/e2e-environment": "file:tests/e2e/env", "@woocommerce/e2e-environment": "file:tests/e2e/env",
@ -84,7 +84,7 @@
"puppeteer": "^2.1.1", "puppeteer": "^2.1.1",
"stylelint": "12.0.1", "stylelint": "12.0.1",
"stylelint-config-wordpress": "16.0.0", "stylelint-config-wordpress": "16.0.0",
"typescript": "3.9.5", "typescript": "3.9.7",
"webpack": "4.44.1", "webpack": "4.44.1",
"webpack-cli": "3.3.12", "webpack-cli": "3.3.12",
"wp-textdomain": "1.0.1" "wp-textdomain": "1.0.1"

View File

@ -14,7 +14,7 @@ module.exports = {
'no-useless-constructor': 'off', 'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 2, '@typescript-eslint/no-useless-constructor': 2,
}, },
'plugins': [ plugins: [
'@typescript-eslint' '@typescript-eslint'
], ],
extends: [ extends: [
@ -22,17 +22,22 @@ module.exports = {
], ],
overrides: [ overrides: [
{ {
'files': [ files: [
'**/*.js', '**/*.js',
'**/*.ts' '**/*.ts'
] ],
settings: {
jsdoc: {
mode: 'typescript',
},
},
}, },
{ {
'files': [ files: [
'**/*.spec.ts', '**/*.spec.ts',
'**/*.test.ts' '**/*.test.ts'
], ],
'rules': { rules: {
'no-console': 'off', 'no-console': 'off',
} }
} }

View File

@ -51,6 +51,8 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
response.headers; response.headers;
// Access the data from the response, in this case, the products. // Access the data from the response, in this case, the products.
response.data; response.data;
}, ( error ) => {
// Handle errors that may have come up.
} ); } );
``` ```

View File

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

View File

@ -672,9 +672,9 @@
} }
}, },
"@types/babel__generator": { "@types/babel__generator": {
"version": "7.6.1", "version": "7.6.2",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz",
"integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.0.0" "@babel/types": "^7.0.0"
@ -691,9 +691,9 @@
} }
}, },
"@types/babel__traverse": { "@types/babel__traverse": {
"version": "7.0.14", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz",
"integrity": "sha512-8w9szzKs14ZtBVuP6Wn7nMLRJ0D6dfB0VEBEyRgxrZ/Ln49aNMykrghM2FaNn4FJRzNppCSa0Rv9pBRM5Xc3wg==", "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.3.0" "@babel/types": "^7.3.0"
@ -4636,9 +4636,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "3.8.3", "version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true "dev": true
}, },
"union-value": { "union-value": {

View File

@ -46,7 +46,7 @@
"jest-mock-extended": "^1.0.10", "jest-mock-extended": "^1.0.10",
"moxios": "0.4.0", "moxios": "0.4.0",
"ts-jest": "25.5.0", "ts-jest": "25.5.0",
"typescript": "3.8.3" "typescript": "3.9.7"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -1,5 +1,17 @@
import { Model } from '../../models/model'; import { Model } from '../../models/model';
import { ModelRepository } from '../model-repository'; import {
CreatesModels,
DeletesChildModels,
DeletesModels,
ListsChildModels,
ListsModels,
ModelRepository,
ModelRepositoryParams,
ReadsChildModels,
ReadsModels,
UpdatesChildModels,
UpdatesModels,
} from '../model-repository';
class DummyModel extends Model { class DummyModel extends Model {
public name: string = ''; public name: string = '';
@ -9,20 +21,103 @@ class DummyModel extends Model {
Object.assign( this, partial ); Object.assign( this, partial );
} }
} }
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
class DummyChildModel extends Model {
public childName: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
type DummyChildParams = ModelRepositoryParams< DummyChildModel, { parent: string }, { childSearch: string }, 'childName' >
describe( 'ModelRepository', () => { describe( 'ModelRepository', () => {
it( 'should list', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( [ model ] );
const repository: ListsModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
callback,
null,
null,
null,
null,
);
const listed = await repository.list( { search: 'test' } );
expect( listed ).toContain( model );
expect( callback ).toHaveBeenCalledWith( { search: 'test' } );
} );
it( 'should list child', async () => {
const model = new DummyChildModel();
const callback = jest.fn().mockResolvedValue( [ model ] );
const repository: ListsChildModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
callback,
null,
null,
null,
null,
);
const listed = await repository.list( { parent: 'test' }, { childSearch: 'test' } );
expect( listed ).toContain( model );
expect( callback ).toHaveBeenCalledWith( { parent: 'test' }, { childSearch: 'test' } );
} );
it( 'should throw error on list without callback', () => {
const repository: ListsModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
null,
null,
);
expect( () => repository.list() ).toThrowError( /not supported/i );
} );
it( 'should create', async () => { it( 'should create', async () => {
const model = new DummyModel(); const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model ); const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( callback, null, null, null ); const repository: CreatesModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
callback,
null,
null,
null,
);
const created = await repository.create( { name: 'test' } ); const created = await repository.create( { name: 'test' } );
expect( created ).toBe( model ); expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { name: 'test' } ); expect( callback ).toHaveBeenCalledWith( { name: 'test' } );
} ); } );
it( 'should create child', async () => {
const model = new DummyChildModel();
const callback = jest.fn().mockResolvedValue( model );
const repository: CreatesModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
null,
callback,
null,
null,
null,
);
const created = await repository.create( { childName: 'test' } );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { childName: 'test' } );
} );
it( 'should throw error on create without callback', () => { it( 'should throw error on create without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null ); const repository: CreatesModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
null,
null,
);
expect( () => repository.create( { name: 'test' } ) ).toThrowError( /not supported/i ); expect( () => repository.create( { name: 'test' } ) ).toThrowError( /not supported/i );
} ); } );
@ -30,15 +125,43 @@ describe( 'ModelRepository', () => {
it( 'should read', async () => { it( 'should read', async () => {
const model = new DummyModel(); const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model ); const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( null, callback, null, null ); const repository: ReadsModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
callback,
null,
null,
);
const created = await repository.read( 1 ); const created = await repository.read( 1 );
expect( created ).toBe( model ); expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( 1 ); expect( callback ).toHaveBeenCalledWith( 1 );
} ); } );
it( 'should read child', async () => {
const model = new DummyChildModel();
const callback = jest.fn().mockResolvedValue( model );
const repository: ReadsChildModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
null,
null,
callback,
null,
null,
);
const created = await repository.read( { parent: 'yes' }, 1 );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { parent: 'yes' }, 1 );
} );
it( 'should throw error on read without callback', () => { it( 'should throw error on read without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null ); const repository: ReadsModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
null,
null,
);
expect( () => repository.read( 1 ) ).toThrowError( /not supported/i ); expect( () => repository.read( 1 ) ).toThrowError( /not supported/i );
} ); } );
@ -46,30 +169,85 @@ describe( 'ModelRepository', () => {
it( 'should update', async () => { it( 'should update', async () => {
const model = new DummyModel(); const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model ); const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( null, null, callback, null ); const repository: UpdatesModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
callback,
null,
);
const updated = await repository.update( 1, { name: 'new-name' } ); const updated = await repository.update( 1, { name: 'new-name' } );
expect( updated ).toBe( model ); expect( updated ).toBe( model );
expect( callback ).toHaveBeenCalledWith( 1, { name: 'new-name' } ); expect( callback ).toHaveBeenCalledWith( 1, { name: 'new-name' } );
} ); } );
it( 'should update child', async () => {
const model = new DummyChildModel();
const callback = jest.fn().mockResolvedValue( model );
const repository: UpdatesChildModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
null,
null,
null,
callback,
null,
);
const updated = await repository.update( { parent: 'test' }, 1, { childName: 'new-name' } );
expect( updated ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { parent: 'test' }, 1, { childName: 'new-name' } );
} );
it( 'should throw error on update without callback', () => { it( 'should throw error on update without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null ); const repository: UpdatesModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
null,
null,
);
expect( () => repository.update( 1, { name: 'new-name' } ) ).toThrowError( /not supported/i ); expect( () => repository.update( 1, { name: 'new-name' } ) ).toThrowError( /not supported/i );
} ); } );
it( 'should delete', async () => { it( 'should delete', async () => {
const callback = jest.fn().mockResolvedValue( true ); const callback = jest.fn().mockResolvedValue( true );
const repository = new ModelRepository< DummyModel >( null, null, null, callback ); const repository: DeletesModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
null,
callback,
);
const success = await repository.delete( 1 ); const success = await repository.delete( 1 );
expect( success ).toBe( true ); expect( success ).toBe( true );
expect( callback ).toHaveBeenCalledWith( 1 ); expect( callback ).toHaveBeenCalledWith( 1 );
} ); } );
it( 'should delete child', async () => {
const callback = jest.fn().mockResolvedValue( true );
const repository: DeletesChildModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
null,
null,
null,
null,
callback,
);
const success = await repository.delete( { parent: 'yes' }, 1 );
expect( success ).toBe( true );
expect( callback ).toHaveBeenCalledWith( { parent: 'yes' }, 1 );
} );
it( 'should throw error on delete without callback', () => { it( 'should throw error on delete without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null ); const repository: DeletesModels< DummyModelParams > = new ModelRepository< DummyModelParams >(
null,
null,
null,
null,
null,
);
expect( () => repository.delete( 1 ) ).toThrowError( /not supported/i ); expect( () => repository.delete( 1 ) ).toThrowError( /not supported/i );
} ); } );

View File

@ -1,69 +1,247 @@
import { Model } from '../models/model'; import { Model, ModelID } from '../models/model';
/**
* An interface for describing the shape of parent identifiers for repositories.
*
* @typedef ModelParentID
* @alias Object.<string,ModelID>
*/
interface ModelParentID {
[ key: number ]: ModelID
}
/**
* This type describes the structure of different kinds of data that is extracted
* for use in the repository to provide type-safety to repository actions.
*/
export interface ModelRepositoryParams<
T extends Model = never,
// @ts-ignore
ParentID extends ModelID | ModelParentID = never,
// @ts-ignore
ListParams = never,
// @ts-ignore
UpdateParams extends keyof T = never,
> {
// Since TypeScript's type system is structural we need to add something to this type to prevent
// it from matching with everything else (since it is an empty interface).
thisTypeIsDeclarativeOnly: string;
}
/**
* These helpers will extract information about a model from its repository params to be used in the repository.
*/
type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never;
type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never;
type ListParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, any, infer X > ] ? X : never;
type UpdateParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer C, any, any, infer X > ] ?
( [ X ] extends [ keyof C ] ? Pick< C, X > : never ) :
never;
type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
/**
* A callback for listing models using a data source.
*
* @callback ListFn
* @param {L} [params] The list parameters for the query.
* @return {Promise.<Array.<T>>} Resolves to an array of created models.
* @template {Model} T
* @template L
*/
export type ListFn< T extends ModelRepositoryParams > = ( params?: ListParams< T > ) => Promise< ModelClass< T >[] >;
/**
* A callback for listing child models using a data source.
*
* @callback ListChildFn
* @param {P} parent The parent identifier for the model.
* @param {L} [params] The list parameters for the query.
* @return {Promise.<Array.<T>>} Resolves to an array of created models.
* @template {Model} T
* @template {ModelParentID} P
* @template L
*/
export type ListChildFn< T extends ModelRepositoryParams > = (
parent: ParentID< T >,
params?: ListParams< T >
) => Promise< ModelClass< T >[] >;
/** /**
* A callback for creating a model using a data source. * A callback for creating a model using a data source.
* *
* @callback CreateFn * @callback CreateFn
* @param {Object} properties The properties of the model to create. * @param {Partial.<T>} properties The properties of the model to create.
* @return {Promise.<Model>} Resolves to the created model. * @return {Promise.<T>} Resolves to the created model.
* @template {Model} T
*/ */
export type CreateFn< T > = ( properties: Partial< T > ) => Promise< T >; export type CreateFn< T extends ModelRepositoryParams > = ( properties: Partial< ModelClass< T > > ) => Promise< ModelClass< T > >;
/** /**
* A callback for reading a model using a data source. * A callback for reading a model using a data source.
* *
* @callback ReadFn * @callback ReadFn
* @param {number|Object} id The ID or object used to find the model. * @param {ModelID} id The ID of the model.
* @return {Promise.<Model>} Resolves to the read model. * @return {Promise.<T>} Resolves to the read model.
* @template {Model} T
*/ */
export type ReadFn< IDParam, T > = ( id: IDParam ) => Promise< T >; export type ReadFn< T extends ModelRepositoryParams > = ( id: ModelID ) => Promise< ModelClass< T > >;
/**
* A callback for reading a child model using a data source.
*
* @callback ReadChildFn
* @param {P} parent The parent identifier for the model.
* @param {ModelID} childID The ID of the model.
* @return {Promise.<T>} Resolves to the read model.
* @template {Model} T
* @template {ModelParentID} P
*/
export type ReadChildFn< T extends ModelRepositoryParams > = ( parent: ParentID< T >, childID: ModelID ) => Promise< ModelClass< T > >;
/** /**
* A callback for updating a model using a data source. * A callback for updating a model using a data source.
* *
* @callback UpdateFn * @callback UpdateFn
* @param {number|Object} id The ID or object used to find the model. * @param {ModelID} id The ID of the model.
* @return {Promise.<Model>} Resolves to the updated model. * @param {Partial.<T>} properties The properties to update.
* @return {Promise.<T>} Resolves to the updated model.
* @template {Model} T
*/ */
export type UpdateFn< IDParam, T > = ( id: IDParam, properties: Partial< T > ) => Promise< T >; export type UpdateFn< T extends ModelRepositoryParams > = (
id: ModelID,
properties: UpdateParams< T >,
) => Promise< ModelClass< T > >;
/**
* A callback for updating a child model using a data source.
*
* @callback UpdateChildFn
* @param {P} parent The parent identifier for the model.
* @param {ModelID} childID The ID of the model.
* @param {Partial.<T>} properties The properties to update.
* @return {Promise.<T>} Resolves to the updated model.
* @template {Model} T
* @template {ModelParentID} P
*/
export type UpdateChildFn< T extends ModelRepositoryParams > = (
parent: ParentID< T >,
childID: ModelID,
properties: UpdateParams< T >,
) => Promise< ModelClass< T > >;
/** /**
* A callback for deleting a model from a data source. * A callback for deleting a model from a data source.
* *
* @callback DeleteFn * @callback DeleteFn
* @param {number|Object} id The ID or object used to find the model. * @param {ModelID} id The ID of the model.
* @return {Promise.<boolean>} Resolves to true once the model has been deleted. * @return {Promise.<boolean>} Resolves to true once the model has been deleted.
*/ */
export type DeleteFn< IDParam > = ( id: IDParam ) => Promise< boolean >; export type DeleteFn = ( id: ModelID ) => Promise< boolean >;
/**
* A callback for deleting a child model from a data source.
*
* @callback DeleteChildFn
* @param {P} parent The parent identifier for the model.
* @param {ModelID} childID The ID of the model.
* @return {Promise.<boolean>} Resolves to true once the model has been deleted.
* @template {ModelParentID} P
*/
export type DeleteChildFn< T extends ModelRepositoryParams > = ( parent: ParentID< T >, childID: ModelID ) => Promise< boolean >;
/**
* An interface for repositories that can list models.
*
* @typedef ListsModels
* @property {ListFn.<T,L>} list Lists models using the repository.
* @template {Model} T
* @template L
*/
export interface ListsModels< T extends ModelRepositoryParams > {
list( params?: HasParent< T, never, ListParams< T > > ): Promise< ModelClass< T >[] >;
}
/**
* An interface for repositories that can list child models.
*
* @typedef ListsChildModels
* @property {ListChildFn.<T,P,L>} list Lists models using the repository.
* @template {Model} T
* @template {ModelParentID} P
* @template L
*/
export interface ListsChildModels< T extends ModelRepositoryParams > {
list(
parent: HasParent< T, ParentID< T >, never >,
params?: HasParent< T, ListParams< T >, never >,
): Promise< ModelClass< T >[] >;
}
/** /**
* An interface for repositories that can create models. * An interface for repositories that can create models.
* *
* @typedef CreatesModels * @typedef CreatesModels
* @property {CreateFn} create Creates a model using the repository. * @property {CreateFn.<T>} create Creates a model using the repository.
* @template {Model} T
*/ */
export interface CreatesModels< T extends Model > { export interface CreatesModels< T extends ModelRepositoryParams > {
create( properties: Partial< T > ): Promise< T >; create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > >;
} }
/** /**
* An interface for repositories that can read models. * An interface for repositories that can read models.
* *
* @typedef ReadsModels * @typedef ReadsModels
* @property {ReadFn} read Reads a model using the repository. * @property {ReadFn.<T>} read Reads a model using the repository.
* @template {Model} T
*/ */
export interface ReadsModels< T extends Model, IDParam = number > { export interface ReadsModels< T extends ModelRepositoryParams > {
read( id: IDParam ): Promise< T >; read( id: HasParent< T, never, ModelID > ): Promise< ModelClass< T > >;
}
/**
* An interface for repositories that can read models that are children.
*
* @typedef ReadsChildModels
* @property {ReadChildFn.<T,P>} read Reads a model using the repository.
* @template {Model} T
* @template {ModelParentID} P
*/
export interface ReadsChildModels< T extends ModelRepositoryParams > {
read(
parent: HasParent< T, ParentID< T >, never >,
childID: HasParent< T, ModelID, never >,
): Promise< ModelClass< T > >;
} }
/** /**
* An interface for repositories that can update models. * An interface for repositories that can update models.
* *
* @typedef UpdatesModels * @typedef UpdatesModels
* @property {UpdateFn} update Updates a model using the repository. * @property {UpdateFn.<T>} update Updates a model using the repository.
* @template {Model} T
*/ */
export interface UpdatesModels< T extends Model, IDParam = number > { export interface UpdatesModels< T extends ModelRepositoryParams > {
update( id: IDParam, properties: Partial< T > ): Promise< T >; update(
id: HasParent< T, never, ModelID >,
properties: HasParent< T, never, UpdateParams< T > >,
): Promise< ModelClass< T > >;
}
/**
* An interface for repositories that can update models.
*
* @typedef UpdatesChildModels
* @property {UpdateChildFn.<T,P>} update Updates a model using the repository.
* @template {Model} T
* @template {ModelParentID} P
*/
export interface UpdatesChildModels< T extends ModelRepositoryParams > {
update(
parent: HasParent< T, ParentID< T >, never >,
childID: HasParent< T, ModelID, never >,
properties: HasParent< T, UpdateParams< T >, never >,
): Promise< ModelClass< T > >;
} }
/** /**
@ -72,24 +250,54 @@ export interface UpdatesModels< T extends Model, IDParam = number > {
* @typedef DeletesModels * @typedef DeletesModels
* @property {DeleteFn} delete Deletes a model using the repository. * @property {DeleteFn} delete Deletes a model using the repository.
*/ */
export interface DeletesModels< IDParam = number > { export interface DeletesModels< T extends ModelRepositoryParams > {
delete( id: IDParam ): Promise< boolean >; delete( id: HasParent< T, never, ModelID > ): Promise< boolean >;
}
/**
* An interface for repositories that can delete models.
*
* @typedef DeletesModels
* @property {DeleteChildFn.<P>} delete Deletes a model using the repository.
* @template {ModelParentID} P
*/
export interface DeletesChildModels< T extends ModelRepositoryParams > {
delete(
parent: HasParent< T, ParentID< T >, never >,
childID: HasParent< T, ModelID, never >,
): Promise< boolean >;
} }
/** /**
* A class for performing CRUD operations on models using a number of internal hooks. * 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 * Note that if a model does not support a given operation then it will throw an
* error when attempting to perform that action. * error when attempting to perform that action.
*
* @template {Model} T
* @template {ModelParentID} P
* @template {Object} L
*/ */
export class ModelRepository< T extends Model, IDParam = number > implements export class ModelRepository< T extends ModelRepositoryParams > implements
CreatesModels< T >, ListsModels< T >,
ReadsModels< T, IDParam >, ListsChildModels< T >,
UpdatesModels< T, IDParam >, ReadsModels< T >,
DeletesModels< IDParam > { ReadsChildModels< T >,
UpdatesModels< T >,
UpdatesChildModels< T >,
DeletesModels< T >,
DeletesChildModels< T > {
/**
* The hook used to list models.
*
* @type {ListFn.<T,P,L>|ListChildFn<T,P,L>}
* @private
*/
private readonly listHook: HasParent< T, ListChildFn< T >, ListFn< T > > | null;
/** /**
* The hook used to create models * The hook used to create models
* *
* @type {CreateFn} * @type {CreateFn.<T>}
* @private * @private
*/ */
private readonly createHook: CreateFn< T > | null; private readonly createHook: CreateFn< T > | null;
@ -97,41 +305,44 @@ export class ModelRepository< T extends Model, IDParam = number > implements
/** /**
* The hook used to read models. * The hook used to read models.
* *
* @type {ReadFn} * @type {ReadFn.<T>|ReadChildFn.<T,P>}
* @private * @private
*/ */
private readonly readHook: ReadFn< IDParam, T > | null; private readonly readHook: HasParent< T, ReadChildFn< T >, ReadFn< T > > | null;
/** /**
* The hook used to update models. * The hook used to update models.
* *
* @type {UpdateFn} * @type {UpdateFn.<T>|UpdateChildFn.<T,P>}
* @private * @private
*/ */
private readonly updateHook: UpdateFn< IDParam, T > | null; private readonly updateHook: HasParent< T, UpdateChildFn< T >, UpdateFn< T > > | null;
/** /**
* The hook used to delete models. * The hook used to delete models.
* *
* @type {DeleteFn} * @type {DeleteFn|DeleteChildFn.<P>}
* @private * @private
*/ */
private readonly deleteHook: DeleteFn< IDParam > | null; private readonly deleteHook: HasParent< T, DeleteChildFn< T >, DeleteFn > | null;
/** /**
* Creates a new repository instance. * Creates a new repository instance.
* *
* @param {CreateFn|null} createHook The hook for model creation. * @param {ListFn.<T,L>|ListChildFn<T,P,L>} listHook The hook for model listing.
* @param {ReadFn|null} readHook The hook for model reading. * @param {CreateFn.<T>|null} createHook The hook for model creation.
* @param {UpdateFn|null} updateHook The hook for model updating. * @param {ReadFn.<T>|ReadChildFn.<T,P>|null} readHook The hook for model reading.
* @param {DeleteFn|null} deleteHook The hook for model deletion. * @param {UpdateFn.<T>|UpdateChildFn.<T,P>|null} updateHook The hook for model updating.
* @param {DeleteFn|DeleteChildFn.<P>|null} deleteHook The hook for model deletion.
*/ */
public constructor( public constructor(
listHook: HasParent< T, ListChildFn< T >, ListFn< T > > | null,
createHook: CreateFn< T > | null, createHook: CreateFn< T > | null,
readHook: ReadFn< IDParam, T > | null, readHook: HasParent< T, ReadChildFn< T >, ReadFn< T > > | null,
updateHook: UpdateFn< IDParam, T > | null, updateHook: HasParent< T, UpdateChildFn< T >, UpdateFn< T > > | null,
deleteHook: DeleteFn< IDParam > | null, deleteHook: HasParent< T, DeleteChildFn< T >, DeleteFn > | null,
) { ) {
this.listHook = listHook;
this.createHook = createHook; this.createHook = createHook;
this.readHook = readHook; this.readHook = readHook;
this.updateHook = updateHook; this.updateHook = updateHook;
@ -139,12 +350,39 @@ export class ModelRepository< T extends Model, IDParam = number > implements
} }
/** /**
* Creates the given model. * Lists models using the repository.
* *
* @param {Object} properties The properties for the model we'd like to create. * @param {L|P} [paramsOrParent] The params for the lookup or the parent to list if the model is a child.
* @return {Promise.<Model>} A promise that resolves to the model after creation. * @param {L} [params] The params when using the parent.
* @return {Promise.<Array.<T>>} Resolves to the listed models.
*/ */
public create( properties: Partial< T > ): Promise< T > { public list(
paramsOrParent?: HasParent< T, ParentID< T >, ListParams< T > >,
params?: HasParent< T, ListParams< T >, never >,
): Promise< ModelClass< T >[] > {
if ( ! this.listHook ) {
throw new Error( 'The \'list\' operation is not supported on this model.' );
}
if ( params === undefined ) {
return ( this.listHook as ListFn< T > )(
paramsOrParent as ListParams< T >,
);
}
return ( this.listHook as ListChildFn< T > )(
paramsOrParent as ParentID< T >,
params,
);
}
/**
* Creates a new model using the repository.
*
* @param {Partial.<T>} properties The properties to create the model with.
* @return {Promise.<T>} Resolves to the created model.
*/
public create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > > {
if ( ! this.createHook ) { if ( ! this.createHook ) {
throw new Error( 'The \'create\' operation is not supported on this model.' ); throw new Error( 'The \'create\' operation is not supported on this model.' );
} }
@ -153,45 +391,87 @@ export class ModelRepository< T extends Model, IDParam = number > implements
} }
/** /**
* Reads the given model. * Reads a model using the repository.
* *
* @param {number|Object} id The identifier for the model to read. * @param {ModelID|P} idOrParent The ID of the model or its parent if the model is a child.
* @return {Promise.<Model>} A promise that resolves to the model. * @param {ModelID} [childID] The ID of the model when using the parent.
* @return {Promise.<T>} Resolves to the loaded model.
*/ */
public read( id: IDParam ): Promise< T > { public read(
idOrParent: HasParent< T, ParentID< T >, ModelID >,
childID?: HasParent< T, ModelID, never >,
): Promise< ModelClass< T > > {
if ( ! this.readHook ) { if ( ! this.readHook ) {
throw new Error( 'The \'read\' operation is not supported on this model.' ); throw new Error( 'The \'read\' operation is not supported on this model.' );
} }
return this.readHook( id ); if ( childID === undefined ) {
return ( this.readHook as ReadFn< T > )(
idOrParent as ModelID,
);
}
return ( this.readHook as ReadChildFn< T > )(
idOrParent as ParentID< T >,
childID,
);
} }
/** /**
* Updates the given model. * Updates the model's properties using the repository.
* *
* @param {number|Object} id The identifier for the model to create. * @param {ModelID|P} idOrParent The ID of the model or its parent if the model is a child.
* @param {Object} properties The model properties that we'd like to update. * @param {Partial.<T>|ModelID} propertiesOrChildID The properties for the model or the ID when using the parent.
* @return {Promise.<Model>} A promise that resolves to the model after updating. * @param {Partial.<T>} [properties] The properties for child models.
* @return {Promise.<T>} Resolves to the updated model.
*/ */
public update( id: IDParam, properties: Partial< T > ): Promise< T > { public update(
idOrParent: HasParent< T, ParentID< T >, ModelID >,
propertiesOrChildID: HasParent< T, ModelID, UpdateParams< T > >,
properties?: HasParent< T, UpdateParams< T >, never >,
): Promise< ModelClass< T > > {
if ( ! this.updateHook ) { if ( ! this.updateHook ) {
throw new Error( 'The \'update\' operation is not supported on this model.' ); throw new Error( 'The \'update\' operation is not supported on this model.' );
} }
return this.updateHook( id, properties ); if ( properties === undefined ) {
return ( this.updateHook as UpdateFn< T > )(
idOrParent as ModelID,
propertiesOrChildID as UpdateParams< T >,
);
}
return ( this.updateHook as UpdateChildFn< T > )(
idOrParent as ParentID< T >,
propertiesOrChildID as ModelID,
properties,
);
} }
/** /**
* Deletes the given model. * Deletes a model using the repository.
* *
* @param {number|Object} id The identifier for the model to delete. * @param {ModelID|P} idOrParent The ID of the model or its parent if the model is a child.
* @return {Promise.<boolean>} A promise that resolves to "true" on success. * @param {ModelID} [childID] The ID of the model when using the parent.
* @return {Promise.<T>} Resolves to the loaded model.
*/ */
public delete( id: IDParam ): Promise< boolean > { public delete(
idOrParent: HasParent< T, ParentID< T >, ModelID >,
childID?: HasParent< T, ModelID, never >,
): Promise< boolean > {
if ( ! this.deleteHook ) { if ( ! this.deleteHook ) {
throw new Error( 'The \'delete\' operation is not supported on this model.' ); throw new Error( 'The \'delete\' operation is not supported on this model.' );
} }
return this.deleteHook( id ); if ( childID === undefined ) {
return ( this.deleteHook as DeleteFn )(
idOrParent as ModelID,
);
}
return ( this.deleteHook as DeleteChildFn< T > )(
idOrParent as ParentID< T >,
childID,
);
} }
} }

View File

@ -0,0 +1,51 @@
import axios, { AxiosInstance } from 'axios';
import * as moxios from 'moxios';
import { AxiosInterceptor } from '../axios-interceptor';
class TestInterceptor extends AxiosInterceptor {}
describe( 'AxiosInterceptor', () => {
let interceptors: TestInterceptor[];
let axiosInstance: AxiosInstance;
beforeEach( () => {
axiosInstance = axios.create();
moxios.install( axiosInstance );
interceptors = [];
} );
afterEach( () => {
for ( const interceptor of interceptors ) {
interceptor.stop( axiosInstance );
}
moxios.uninstall( axiosInstance );
} );
it( 'should not break interceptor chaining for success', async () => {
moxios.stubRequest( 'http://test.test', { status: 200 } );
interceptors.push( new TestInterceptor() );
interceptors.push( new TestInterceptor() );
interceptors.push( new TestInterceptor() );
for ( const interceptor of interceptors ) {
interceptor.start( axiosInstance );
}
const response = await axiosInstance.get( 'http://test.test' );
expect( response.status ).toBe( 200 );
} );
it( 'should not break interceptor chaining for errors', async () => {
moxios.stubRequest( 'http://test.test', { status: 401 } );
interceptors.push( new TestInterceptor() );
interceptors.push( new TestInterceptor() );
interceptors.push( new TestInterceptor() );
for ( const interceptor of interceptors ) {
interceptor.start( axiosInstance );
}
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toBeInstanceOf( Error );
} );
} );

View File

@ -49,9 +49,7 @@ describe( 'AxiosResponseInterceptor', () => {
responseText: JSON.stringify( { code: 'error_code', message: 'value' } ), responseText: JSON.stringify( { code: 'error_code', message: 'value' } ),
} ); } );
const response = await axiosInstance.get( 'http://test.test' ); await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject( {
expect( response ).toMatchObject( {
statusCode: 404, statusCode: 404,
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',

View File

@ -0,0 +1,23 @@
import { buildURL } from '../utils';
describe( 'buildURL', () => {
it( 'should use base when given no url', () => {
const url = buildURL( { baseURL: 'http://test.test' } );
expect( url ).toBe( 'http://test.test' );
} );
it( 'should use url when given absolute', () => {
const url = buildURL( { baseURL: 'http://test.test', url: 'http://override.test' } );
expect( url ).toBe( 'http://override.test' );
} );
it( 'should combine base and url', () => {
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
expect( url ).toBe( 'http://test.test/yes/test' );
} );
it( 'should combine base and url with trailing/leading slashes', () => {
const url = buildURL( { baseURL: 'http://test.test/////', url: '////yes/test' } );
expect( url ).toBe( 'http://test.test/yes/test' );
} );
} );

View File

@ -8,7 +8,7 @@ import { AxiosResponseInterceptor } from './axios-response-interceptor';
*/ */
export class AxiosClient implements HTTPClient { export class AxiosClient implements HTTPClient {
/** /**
* An instance of the axios client for making HTTP requests. * An instance of the Axios client for making HTTP requests.
* *
* @type {AxiosInstance} * @type {AxiosInstance}
* @private * @private

View File

@ -15,7 +15,7 @@ type ActiveInterceptor = {
} }
/** /**
* A base class for encapsulating the start and stop functionality required by all axios interceptors. * A base class for encapsulating the start and stop functionality required by all Axios interceptors.
*/ */
export abstract class AxiosInterceptor { export abstract class AxiosInterceptor {
/** /**
@ -61,7 +61,7 @@ export abstract class AxiosInterceptor {
/** /**
* An interceptor method for handling requests before they are made to the server. * An interceptor method for handling requests before they are made to the server.
* *
* @param {AxiosRequestConfig} config The axios request options. * @param {AxiosRequestConfig} config The Axios request options.
*/ */
protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig { protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig {
return config; return config;
@ -70,7 +70,7 @@ export abstract class AxiosInterceptor {
/** /**
* An interceptor method for handling successful responses. * An interceptor method for handling successful responses.
* *
* @param {AxiosResponse} response The response from the axios client. * @param {*} response The response from the Axios client.
*/ */
protected onResponseSuccess( response: AxiosResponse ): any { protected onResponseSuccess( response: AxiosResponse ): any {
return response; return response;
@ -79,9 +79,9 @@ export abstract class AxiosInterceptor {
/** /**
* An interceptor method for handling response failures. * An interceptor method for handling response failures.
* *
* @param {*} error The error that occurred. * @param {Promise} error The error that occurred.
*/ */
protected onResponseRejected( error: any ): any { protected onResponseRejected( error: any ): any {
return error; throw error;
} }
} }

View File

@ -2,6 +2,7 @@ import type { AxiosRequestConfig } from 'axios';
import * as createHmac from 'create-hmac'; import * as createHmac from 'create-hmac';
import * as OAuth from 'oauth-1.0a'; import * as OAuth from 'oauth-1.0a';
import { AxiosInterceptor } from './axios-interceptor'; import { AxiosInterceptor } from './axios-interceptor';
import { buildURL } from './utils';
/** /**
* A utility class for managing the lifecycle of an authentication interceptor. * A utility class for managing the lifecycle of an authentication interceptor.
@ -43,7 +44,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
* @return {AxiosRequestConfig} The request with the additional authorization headers. * @return {AxiosRequestConfig} The request with the additional authorization headers.
*/ */
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig { protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
const url = ( request.baseURL || '' ) + ( request.url || '' ); const url = buildURL( request );
if ( url.startsWith( 'https' ) ) { if ( url.startsWith( 'https' ) ) {
request.auth = { request.auth = {
username: this.oauth.consumer.key, username: this.oauth.consumer.key,

View File

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

View File

@ -0,0 +1,24 @@
import { AxiosRequestConfig } from 'axios';
/**
* Given an Axios request config this function generates the URL that Axios will
* use to make the request.
*
* @param {AxiosRequestConfig} request The Axios request we're building the URL for.
* @return {string} The merged URL.
*/
export function buildURL( request: AxiosRequestConfig ): string {
const base = request.baseURL || '';
if ( ! request.url ) {
return base;
}
// Axios ignores the base when the URL is absolute.
const url = request.url;
if ( ! base || url.match( /^([a-z][a-z\d+\-.]*:)?\/\/[^\/]/i ) ) {
return url;
}
// Remove trailing slashes from the base and leading slashes from the URL so we can combine them consistently.
return base.replace( /\/+$/, '' ) + '/' + url.replace( /^\/+/, '' );
}

View File

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

View File

@ -1 +1,4 @@
export { SimpleProduct } from './products/simple-product'; export { SimpleProduct } from './products/simple-product';
export { SettingGroup } from './settings/setting-group';
export { Setting } from './settings/setting';

View File

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

View File

@ -1,7 +1,21 @@
import { AbstractProduct } from './abstract-product'; import { AbstractProduct } from './abstract-product';
import { HTTPClient } from '../../http'; import { HTTPClient } from '../../http';
import { CreatesModels } from '../../framework/model-repository';
import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product'; import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product';
import { CreatesModels, ModelRepositoryParams } from '../../framework/model-repository';
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, never, 'regularPrice' >;
/**
* An interface for creating simple products using the repository.
*
* @typedef CreatesSimpleProducts
* @alias CreatesModels.<SimpleProduct>
*/
export type CreatesSimpleProducts = CreatesModels< SimpleProductRepositoryParams >;
/** /**
* A simple product object. * A simple product object.
@ -21,9 +35,8 @@ export class SimpleProduct extends AbstractProduct {
* Creates a model repository configured for communicating via the REST API. * Creates a model repository configured for communicating via the REST API.
* *
* @param {HTTPClient} httpClient The client for communicating via HTTP. * @param {HTTPClient} httpClient The client for communicating via HTTP.
* @return {CreatesModels} The created repository.
*/ */
public static restRepository( httpClient: HTTPClient ): CreatesModels< SimpleProduct > { public static restRepository( httpClient: HTTPClient ): ReturnType< typeof simpleProductRESTRepository > {
return simpleProductRESTRepository( httpClient ); return simpleProductRESTRepository( httpClient );
} }
} }

View File

@ -0,0 +1,63 @@
import { Model, ModelID } from '../model';
import { HTTPClient } from '../../http';
import { settingGroupRESTRepository } from '../../repositories/rest/settings/setting-group';
import { ListsModels, ModelRepositoryParams } from '../../framework/model-repository';
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type SettingGroupRepositoryParams = ModelRepositoryParams< SettingGroup >;
/**
* An interface for listing setting groups using the repository.
*
* @typedef ListsSettingGroups
* @alias ListsModels.<SettingGroup>
*/
export type ListsSettingGroups = ListsModels< SettingGroupRepositoryParams >;
/**
* A settings group object.
*/
export class SettingGroup extends Model {
/**
* The label of the setting group.
*
* @type {string}
*/
public readonly label: string = '';
/**
* The description of the setting group.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The ID of the group this is a child of.
*
* @type {ModelID|null}
*/
public readonly parentID: ModelID | null = null;
/**
* Creates a new setting group instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< SettingGroup > = {} ) {
super();
Object.assign( this, properties );
}
/**
* Returns the repository for interacting with this type of model.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof settingGroupRESTRepository > {
return settingGroupRESTRepository( httpClient );
}
}

View File

@ -0,0 +1,110 @@
import { Model, ModelID } from '../model';
import { HTTPClient } from '../../http';
import { settingRESTRepository } from '../../repositories/rest/settings/setting';
import {
ModelRepositoryParams,
ListsChildModels,
ReadsChildModels,
UpdatesChildModels,
} from '../../framework/model-repository';
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type SettingRepositoryParams = ModelRepositoryParams< Setting, ModelID, never, 'value' >;
/**
* An interface for listing settings using the repository.
*
* @typedef ListsSettings
* @alias ListsChildModels.<Setting,SettingParentID>
*/
export type ListsSettings = ListsChildModels< SettingRepositoryParams >;
/**
* An interface for reading settings using the repository.
*
* @typedef ReadsSettings
* @alias ReadsChildModels.<Setting,SettingParentID>
*/
export type ReadsSettings = ReadsChildModels< SettingRepositoryParams >;
/**
* An interface for updating settings using the repository.
*
* @typedef UpdatesSettings
* @alias UpdatesChildModels.<Setting,SettingParentID>
*/
export type UpdatesSettings = UpdatesChildModels< SettingRepositoryParams >;
/**
* The default types of settings that are available.
*/
type SettingType = 'text' | 'select' | 'multiselect' | 'checkbox' | 'number';
/**
* A setting object.
*/
export class Setting extends Model {
/**
* The label of the setting.
*
* @type {string}
*/
public readonly label: string = '';
/**
* The description of the setting.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The type of the setting.
*
* @type {string}
*/
public readonly type: string | SettingType = '';
/**
* The options of the setting, if it has any.
*
* @type {Object.<string, string>|null}
*/
public readonly options: { [key: string]: string } | undefined;
/**
* The default value for the setting.
*
* @type {string}
*/
public readonly default: string = '';
/**
* The current value of the setting.
*
* @type {string}
*/
public readonly value: string = '';
/**
* Creates a new setting instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< Setting > = {} ) {
super();
Object.assign( this, properties );
}
/**
* Returns the repository for interacting with this type of model.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof settingRESTRepository > {
return settingRESTRepository( httpClient );
}
}

View File

@ -2,11 +2,10 @@ import { simpleProductRESTRepository } from '../simple-product';
import { mock, MockProxy } from 'jest-mock-extended'; import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http'; import { HTTPClient, HTTPResponse } from '../../../../http';
import { SimpleProduct } from '../../../../models'; import { SimpleProduct } from '../../../../models';
import { CreatesModels } from '../../../../framework/model-repository';
describe( 'simpleProductRESTRepository', () => { describe( 'simpleProductRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >; let httpClient: MockProxy< HTTPClient >;
let repository: CreatesModels< SimpleProduct >; let repository: ReturnType< typeof simpleProductRESTRepository >;
beforeEach( () => { beforeEach( () => {
httpClient = mock< HTTPClient >(); httpClient = mock< HTTPClient >();

View File

@ -1,14 +1,9 @@
import { HTTPClient } from '../../../http'; import { HTTPClient } from '../../../http';
import { CreateFn, CreatesModels, ModelRepository } from '../../../framework/model-repository'; import { CreateFn, ModelRepository } from '../../../framework/model-repository';
import { SimpleProduct } from '../../../models'; import { SimpleProduct } from '../../../models';
import { CreatesSimpleProducts, SimpleProductRepositoryParams } from '../../../models/products/simple-product';
/** function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProductRepositoryParams > {
* 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 ) => { return async ( properties ) => {
const response = await httpClient.post( const response = await httpClient.post(
'/wc/v3/products', '/wc/v3/products',
@ -31,10 +26,11 @@ function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProduct > {
* Creates a new ModelRepository instance for interacting with models via the REST API. * 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. * @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. * @return {CreatesSimpleProducts} The created repository.
*/ */
export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesModels< SimpleProduct > { export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesSimpleProducts {
return new ModelRepository( return new ModelRepository(
null,
restCreate( httpClient ), restCreate( httpClient ),
null, null,
null, null,

View File

@ -0,0 +1,37 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { settingGroupRESTRepository } from '../setting-group';
describe( 'settingGroupRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof settingGroupRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = settingGroupRESTRepository( httpClient );
} );
it( 'should list', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'group_1',
label: 'Test Group 1',
},
{
id: 'group_2',
label: 'Test Group 2',
},
],
) );
const list = await repository.list();
expect( list ).toHaveLength( 2 );
expect( list[ 0 ] ).toMatchObject( { id: 'group_1', label: 'Test Group 1' } );
expect( list[ 1 ] ).toMatchObject( { id: 'group_2', label: 'Test Group 2' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings' );
} );
} );

View File

@ -0,0 +1,73 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { settingRESTRepository } from '../setting';
describe( 'settingGroupRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof settingRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = settingRESTRepository( httpClient );
} );
it( 'should list', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'setting_1',
label: 'Test Setting 1',
},
{
id: 'setting_2',
label: 'Test Setting 2',
},
],
) );
const list = await repository.list( 'general' );
expect( list ).toHaveLength( 2 );
expect( list[ 0 ] ).toMatchObject( { id: 'setting_1', label: 'Test Setting 1' } );
expect( list[ 1 ] ).toMatchObject( { id: 'setting_2', label: 'Test Setting 2' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general' );
} );
it( 'should read', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'setting_1',
label: 'Test Setting',
},
) );
const read = await repository.read( 'general', 'setting_1' );
expect( read ).toMatchObject( { id: 'setting_1', label: 'Test Setting' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general/setting_1' );
} );
it( 'should update', async () => {
httpClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'setting_1',
label: 'Test Setting',
value: 'updated-value',
},
) );
const updated = await repository.update( 'general', 'setting_1', { value: 'test-value' } );
expect( updated ).toMatchObject( { id: 'setting_1', value: 'updated-value' } );
expect( httpClient.patch ).toHaveBeenCalledWith(
'/wc/v3/settings/general/setting_1',
{ value: 'test-value' },
);
} );
} );

View File

@ -0,0 +1,38 @@
import { HTTPClient } from '../../../http';
import { ListFn, ModelRepository } from '../../../framework/model-repository';
import { SettingGroup } from '../../../models';
import { ListsSettingGroups, SettingGroupRepositoryParams } from '../../../models/settings/setting-group';
function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParams > {
return async () => {
const response = await httpClient.get( '/wc/v3/settings' );
const list: SettingGroup[] = [];
for ( const raw of response.data ) {
list.push( new SettingGroup( {
id: raw.id,
label: raw.label,
description: raw.description,
parentID: raw.parent_id,
} ) );
}
return Promise.resolve( list );
};
}
/**
* 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 {ListsSettingGroups} The created repository.
*/
export function settingGroupRESTRepository( httpClient: HTTPClient ): ListsSettingGroups {
return new ModelRepository(
restList( httpClient ),
null,
null,
null,
null,
);
}

View File

@ -0,0 +1,86 @@
import { HTTPClient } from '../../../http';
import {
ListChildFn,
ModelRepository,
ReadChildFn,
UpdateChildFn,
} from '../../../framework/model-repository';
import { Setting } from '../../../models';
import {
ListsSettings,
ReadsSettings,
SettingRepositoryParams,
UpdatesSettings,
} from '../../../models/settings/setting';
function restList( httpClient: HTTPClient ): ListChildFn< SettingRepositoryParams > {
return async ( parent ) => {
const response = await httpClient.get( '/wc/v3/settings/' + parent );
const list: Setting[] = [];
for ( const raw of response.data ) {
list.push( new Setting( {
id: raw.id,
label: raw.label,
description: raw.description,
type: raw.type,
options: raw.options,
default: raw.default,
value: raw.value,
} ) );
}
return Promise.resolve( list );
};
}
function restRead( httpClient: HTTPClient ): ReadChildFn< SettingRepositoryParams > {
return async ( parent, id ) => {
const response = await httpClient.get( '/wc/v3/settings/' + parent + '/' + id );
return Promise.resolve( new Setting( {
id: response.data.id,
label: response.data.label,
description: response.data.description,
type: response.data.type,
options: response.data.options,
default: response.data.default,
value: response.data.value,
} ) );
};
}
function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryParams > {
return async ( parent, id, params ) => {
const response = await httpClient.patch(
'/wc/v3/settings/' + parent + '/' + id,
params,
);
return Promise.resolve( new Setting( {
id: response.data.id,
label: response.data.label,
description: response.data.description,
type: response.data.type,
options: response.data.options,
default: response.data.default,
value: response.data.value,
} ) );
};
}
/**
* 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 {ListsSettings|ReadsSettings|UpdatesSettings} The created repository.
*/
export function settingRESTRepository( httpClient: HTTPClient ): ListsSettings & ReadsSettings & UpdatesSettings {
return new ModelRepository(
restList( httpClient ),
null,
restRead( httpClient ),
restUpdate( httpClient ),
null,
);
}

View File

@ -0,0 +1,31 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { UpdatesSettings } from '../../models/settings/setting';
import { SettingService } from '../setting-service';
describe( 'SettingService', () => {
let repository: MockProxy< UpdatesSettings >;
let service: SettingService;
beforeEach( () => {
repository = mock< UpdatesSettings >();
service = new SettingService( repository );
} );
it( 'should update address', async () => {
const result = await service.updateStoreAddress(
'line1',
'line2',
'New York',
'US:NY',
'12345',
);
expect( result ).toBeTruthy();
expect( repository.update ).toHaveBeenCalledTimes( 5 );
expect( repository.update ).toHaveBeenCalledWith( 'general', 'woocommerce_store_address', { value: 'line1' } );
expect( repository.update ).toHaveBeenCalledWith( 'general', 'woocommerce_store_address_2', { value: 'line2' } );
expect( repository.update ).toHaveBeenCalledWith( 'general', 'woocommerce_store_city', { value: 'New York' } );
expect( repository.update ).toHaveBeenCalledWith( 'general', 'woocommerce_default_country', { value: 'US:NY' } );
expect( repository.update ).toHaveBeenCalledWith( 'general', 'woocommerce_store_postcode', { value: '12345' } );
} );
} );

View File

@ -0,0 +1 @@
export { SettingService } from './setting-service';

View File

@ -0,0 +1,45 @@
import { Setting, UpdatesSettings } from '../models/settings/setting';
/**
* A service that wraps setting changes in convenient methods.
*/
export class SettingService {
/**
* The repository that will be used to change the settings.
*
* @type {UpdatesSettings}
* @private
*/
private readonly repository: UpdatesSettings;
/**
* Creates a new service class for easily changing store settings.
*
* @param {UpdatesSettings} repository The repository that will be used to change the settings.
*/
public constructor( repository: UpdatesSettings ) {
this.repository = repository;
}
/**
* Updates the address for the store.
*
* @param {string} address1 The first address line.
* @param {string} address2 The second address line.
* @param {string} city The city.
* @param {string} country The country or country/state.
* @param {string} postCode The postal code.
* @return {Promise.<boolean>} Resolves to true if all of the settings are updated.
*/
public updateStoreAddress( address1: string, address2: string, city: string, country: string, postCode: string ): Promise< boolean > {
const promises: Promise< Setting >[] = [];
promises.push( this.repository.update( 'general', 'woocommerce_store_address', { value: address1 } ) );
promises.push( this.repository.update( 'general', 'woocommerce_store_address_2', { value: address2 } ) );
promises.push( this.repository.update( 'general', 'woocommerce_store_city', { value: city } ) );
promises.push( this.repository.update( 'general', 'woocommerce_default_country', { value: country } ) );
promises.push( this.repository.update( 'general', 'woocommerce_store_postcode', { value: postCode } ) );
return Promise.all( promises ).then( () => true );
}
}

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

@ -2601,9 +2601,9 @@
} }
}, },
"@types/babel__generator": { "@types/babel__generator": {
"version": "7.6.1", "version": "7.6.2",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz",
"integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==",
"requires": { "requires": {
"@babel/types": "^7.0.0" "@babel/types": "^7.0.0"
} }
@ -2618,13 +2618,21 @@
} }
}, },
"@types/babel__traverse": { "@types/babel__traverse": {
"version": "7.0.14", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz",
"integrity": "sha512-8w9szzKs14ZtBVuP6Wn7nMLRJ0D6dfB0VEBEyRgxrZ/Ln49aNMykrghM2FaNn4FJRzNppCSa0Rv9pBRM5Xc3wg==", "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==",
"requires": { "requires": {
"@babel/types": "^7.3.0" "@babel/types": "^7.3.0"
} }
}, },
"@types/cheerio": {
"version": "0.22.22",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.22.tgz",
"integrity": "sha512-05DYX4zU96IBfZFY+t3Mh88nlwSMtmmzSYaQkKN48T495VV1dkHSah6qYyDTN5ngaS0i0VonH37m+RuzSM0YiA==",
"requires": {
"@types/node": "*"
}
},
"@types/color-name": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@ -3760,20 +3768,20 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -3795,19 +3803,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -3824,19 +3832,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -3855,20 +3863,20 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -4322,9 +4330,9 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001137", "version": "1.0.30001141",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001137.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001141.tgz",
"integrity": "sha512-54xKQZTqZrKVHmVz0+UvdZR6kQc7pJDgfhsMYDG19ID1BWoNnDMFm5Q3uSBSU401pBvKYMsHAt9qhEDcxmk8aw==" "integrity": "sha512-EHfInJHoQTmlMdVZrEc5gmwPc0zyN/hVufmGHPbVNQwlk7tJfCmQ2ysRZMY2MeleBivALUTyyxXnQjK18XrVpA=="
}, },
"capture-exit": { "capture-exit": {
"version": "2.0.0", "version": "2.0.0",
@ -4976,9 +4984,9 @@
} }
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.572", "version": "1.3.576",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.572.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz",
"integrity": "sha512-TKqdEukCCl7JC20SwEoWTbtnGt4YjfHWAv4tcNky0a9qGo0WdM+Lrd60tps+nkaJCmktKBJjr99fLtEBU1ipWQ==" "integrity": "sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew=="
}, },
"emoji-regex": { "emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
@ -5088,10 +5096,11 @@
} }
}, },
"enzyme-to-json": { "enzyme-to-json": {
"version": "3.5.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.5.0.tgz", "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz",
"integrity": "sha512-clusXRsiaQhG7+wtyc4t7MU8N3zCOgf4eY9+CeSenYzKlFST4lxerfOvnWd4SNaToKhkuba+w6m242YpQOS7eA==", "integrity": "sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==",
"requires": { "requires": {
"@types/cheerio": "^0.22.22",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"react-is": "^16.12.0" "react-is": "^16.12.0"
} }
@ -5105,20 +5114,20 @@
} }
}, },
"es-abstract": { "es-abstract": {
"version": "1.18.0-next.0", "version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0", "is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1", "is-regex": "^1.1.1",
"object-inspect": "^1.8.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -5894,19 +5903,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -6404,20 +6413,20 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -9391,32 +9400,12 @@
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
}, },
"object-is": { "object-is": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz",
"integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==",
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.17.6",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.0",
"is-regex": "^1.1.0",
"object-inspect": "^1.7.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
}
} }
}, },
"object-keys": { "object-keys": {
@ -9454,19 +9443,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -9491,19 +9480,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -9520,19 +9509,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -9559,19 +9548,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -10217,20 +10206,20 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -10976,20 +10965,20 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -11015,19 +11004,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -11044,19 +11033,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }
@ -11566,19 +11555,19 @@
}, },
"dependencies": { "dependencies": {
"es-abstract": { "es-abstract": {
"version": "1.17.6", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1",
"is-callable": "^1.2.0", "is-callable": "^1.2.2",
"is-regex": "^1.1.0", "is-regex": "^1.1.1",
"object-inspect": "^1.7.0", "object-inspect": "^1.8.0",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.0", "object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1", "string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1" "string.prototype.trimstart": "^1.0.1"
} }