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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,17 @@
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 {
public name: string = '';
@ -9,20 +21,103 @@ class DummyModel extends Model {
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', () => {
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 () => {
const model = new DummyModel();
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' } );
expect( created ).toBe( model );
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', () => {
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 );
} );
@ -30,15 +125,43 @@ describe( 'ModelRepository', () => {
it( 'should read', async () => {
const model = new DummyModel();
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 );
expect( created ).toBe( model );
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', () => {
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 );
} );
@ -46,30 +169,85 @@ describe( 'ModelRepository', () => {
it( 'should update', async () => {
const model = new DummyModel();
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' } );
expect( updated ).toBe( model );
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', () => {
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 );
} );
it( 'should delete', async () => {
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 );
expect( success ).toBe( true );
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', () => {
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 );
} );

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.
*
* @callback CreateFn
* @param {Object} properties The properties of the model to create.
* @return {Promise.<Model>} Resolves to the created model.
* @param {Partial.<T>} properties The properties of the model to create.
* @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.
*
* @callback ReadFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<Model>} Resolves to the read model.
* @param {ModelID} id The ID of the 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.
*
* @callback UpdateFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<Model>} Resolves to the updated model.
* @param {ModelID} id The ID of the 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.
*
* @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.
*/
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.
*
* @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 > {
create( properties: Partial< T > ): Promise< T >;
export interface CreatesModels< T extends ModelRepositoryParams > {
create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > >;
}
/**
* An interface for repositories that can read models.
*
* @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 > {
read( id: IDParam ): Promise< T >;
export interface ReadsModels< T extends ModelRepositoryParams > {
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.
*
* @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 > {
update( id: IDParam, properties: Partial< T > ): Promise< T >;
export interface UpdatesModels< T extends ModelRepositoryParams > {
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
* @property {DeleteFn} delete Deletes a model using the repository.
*/
export interface DeletesModels< IDParam = number > {
delete( id: IDParam ): Promise< boolean >;
export interface DeletesModels< T extends ModelRepositoryParams > {
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.
* Note that if a model does not support a given operation then it will throw an
* 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
CreatesModels< T >,
ReadsModels< T, IDParam >,
UpdatesModels< T, IDParam >,
DeletesModels< IDParam > {
export class ModelRepository< T extends ModelRepositoryParams > implements
ListsModels< T >,
ListsChildModels< T >,
ReadsModels< T >,
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
*
* @type {CreateFn}
* @type {CreateFn.<T>}
* @private
*/
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.
*
* @type {ReadFn}
* @type {ReadFn.<T>|ReadChildFn.<T,P>}
* @private
*/
private readonly readHook: ReadFn< IDParam, T > | null;
private readonly readHook: HasParent< T, ReadChildFn< T >, ReadFn< T > > | null;
/**
* The hook used to update models.
*
* @type {UpdateFn}
* @type {UpdateFn.<T>|UpdateChildFn.<T,P>}
* @private
*/
private readonly updateHook: UpdateFn< IDParam, T > | null;
private readonly updateHook: HasParent< T, UpdateChildFn< T >, UpdateFn< T > > | null;
/**
* The hook used to delete models.
*
* @type {DeleteFn}
* @type {DeleteFn|DeleteChildFn.<P>}
* @private
*/
private readonly deleteHook: DeleteFn< IDParam > | null;
private readonly deleteHook: HasParent< T, DeleteChildFn< T >, DeleteFn > | 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.
* @param {ListFn.<T,L>|ListChildFn<T,P,L>} listHook The hook for model listing.
* @param {CreateFn.<T>|null} createHook The hook for model creation.
* @param {ReadFn.<T>|ReadChildFn.<T,P>|null} readHook The hook for model reading.
* @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(
listHook: HasParent< T, ListChildFn< T >, ListFn< T > > | null,
createHook: CreateFn< T > | null,
readHook: ReadFn< IDParam, T > | null,
updateHook: UpdateFn< IDParam, T > | null,
deleteHook: DeleteFn< IDParam > | null,
readHook: HasParent< T, ReadChildFn< T >, ReadFn< T > > | null,
updateHook: HasParent< T, UpdateChildFn< T >, UpdateFn< T > > | null,
deleteHook: HasParent< T, DeleteChildFn< T >, DeleteFn > | null,
) {
this.listHook = listHook;
this.createHook = createHook;
this.readHook = readHook;
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.
* @return {Promise.<Model>} A promise that resolves to the model after creation.
* @param {L|P} [paramsOrParent] The params for the lookup or the parent to list if the model is a child.
* @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 ) {
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.
* @return {Promise.<Model>} A promise that resolves to the model.
* @param {ModelID|P} idOrParent The ID of the model or its parent if the model is a child.
* @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 ) {
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 {Object} properties The model properties that we'd like to update.
* @return {Promise.<Model>} A promise that resolves to the model after updating.
* @param {ModelID|P} idOrParent The ID of the model or its parent if the model is a child.
* @param {Partial.<T>|ModelID} propertiesOrChildID The properties for the model or the ID when using the parent.
* @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 ) {
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.
* @return {Promise.<boolean>} A promise that resolves to "true" on success.
* @param {ModelID|P} idOrParent The ID of the model or its parent if the model is a child.
* @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 ) {
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' } ),
} );
const response = await axiosInstance.get( 'http://test.test' );
expect( response ).toMatchObject( {
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject( {
statusCode: 404,
headers: {
'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 {
/**
* An instance of the axios client for making HTTP requests.
* An instance of the Axios client for making HTTP requests.
*
* @type {AxiosInstance}
* @private
@ -75,7 +75,7 @@ export class AxiosClient implements HTTPClient {
* 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.
* @param {Object} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public put< T = any >(
@ -89,7 +89,7 @@ export class AxiosClient implements HTTPClient {
* Performs a PATCH request.
*
* @param {string} path The path we should query.
* @param {*} data Any parameters that should be passed in the request.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public patch< T = any >(
@ -103,7 +103,7 @@ export class AxiosClient implements HTTPClient {
* 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.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public delete< T = any >(

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 {
/**
@ -61,7 +61,7 @@ export abstract class AxiosInterceptor {
/**
* 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 {
return config;
@ -70,7 +70,7 @@ export abstract class AxiosInterceptor {
/**
* 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 {
return response;
@ -79,9 +79,9 @@ export abstract class AxiosInterceptor {
/**
* 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 {
return error;
throw error;
}
}

View File

@ -2,6 +2,7 @@ import type { AxiosRequestConfig } from 'axios';
import * as createHmac from 'create-hmac';
import * as OAuth from 'oauth-1.0a';
import { AxiosInterceptor } from './axios-interceptor';
import { buildURL } from './utils';
/**
* 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.
*/
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
const url = ( request.baseURL || '' ) + ( request.url || '' );
const url = buildURL( request );
if ( url.startsWith( 'https' ) ) {
request.auth = {
username: this.oauth.consumer.key,

View File

@ -7,26 +7,21 @@ 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.
* @return {HTTPResponse} The HTTP response.
*/
protected onResponseSuccess( response: AxiosResponse ): Promise< HTTPResponse > {
return Promise.resolve< HTTPResponse >(
new HTTPResponse( response.status, response.headers, response.data ),
);
protected onResponseSuccess( response: AxiosResponse ): HTTPResponse {
return 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 > {
protected onResponseRejected( error: any ): never {
// 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 new HTTPResponse( error.response.status, error.response.headers, error.response.data );
}
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

@ -12,7 +12,7 @@ export class HTTPResponse< T = any > {
/**
* The headers from the response.
*
* @type {Object.<string, string|string[]>}
* @type {Object.<string,string|string[]>}
*/
public readonly headers: any;
@ -27,7 +27,7 @@ export class HTTPResponse< T = any > {
* 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.<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 ) {
@ -45,7 +45,7 @@ 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.
* @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 > >;
@ -54,7 +54,7 @@ export interface HTTPClient {
* 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.
* @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 > >;
@ -63,7 +63,7 @@ export interface HTTPClient {
* 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.
* @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 > >;
@ -72,7 +72,7 @@ export interface HTTPClient {
* 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.
* @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 > >;
@ -81,7 +81,7 @@ export interface HTTPClient {
* 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.
* @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

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

View File

@ -1 +1,4 @@
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 {
/**
* 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 { HTTPClient } from '../../http';
import { CreatesModels } from '../../framework/model-repository';
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.
@ -21,9 +35,8 @@ export class SimpleProduct extends AbstractProduct {
* 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 > {
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof simpleProductRESTRepository > {
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 { HTTPClient, HTTPResponse } from '../../../../http';
import { SimpleProduct } from '../../../../models';
import { CreatesModels } from '../../../../framework/model-repository';
describe( 'simpleProductRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: CreatesModels< SimpleProduct >;
let repository: ReturnType< typeof simpleProductRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();

View File

@ -1,14 +1,9 @@
import { HTTPClient } from '../../../http';
import { CreateFn, CreatesModels, ModelRepository } from '../../../framework/model-repository';
import { CreateFn, ModelRepository } from '../../../framework/model-repository';
import { SimpleProduct } from '../../../models';
import { CreatesSimpleProducts, SimpleProductRepositoryParams } from '../../../models/products/simple-product';
/**
* 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 > {
function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProductRepositoryParams > {
return async ( properties ) => {
const response = await httpClient.post(
'/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.
*
* @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(
null,
restCreate( httpClient ),
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": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz",
"integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz",
"integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==",
"requires": {
"@babel/types": "^7.0.0"
}
@ -2618,13 +2618,21 @@
}
},
"@types/babel__traverse": {
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.14.tgz",
"integrity": "sha512-8w9szzKs14ZtBVuP6Wn7nMLRJ0D6dfB0VEBEyRgxrZ/Ln49aNMykrghM2FaNn4FJRzNppCSa0Rv9pBRM5Xc3wg==",
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz",
"integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==",
"requires": {
"@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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@ -3760,20 +3768,20 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true,
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -3795,19 +3803,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -3824,19 +3832,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -3855,20 +3863,20 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true,
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -4322,9 +4330,9 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"caniuse-lite": {
"version": "1.0.30001137",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001137.tgz",
"integrity": "sha512-54xKQZTqZrKVHmVz0+UvdZR6kQc7pJDgfhsMYDG19ID1BWoNnDMFm5Q3uSBSU401pBvKYMsHAt9qhEDcxmk8aw=="
"version": "1.0.30001141",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001141.tgz",
"integrity": "sha512-EHfInJHoQTmlMdVZrEc5gmwPc0zyN/hVufmGHPbVNQwlk7tJfCmQ2ysRZMY2MeleBivALUTyyxXnQjK18XrVpA=="
},
"capture-exit": {
"version": "2.0.0",
@ -4976,9 +4984,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.572",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.572.tgz",
"integrity": "sha512-TKqdEukCCl7JC20SwEoWTbtnGt4YjfHWAv4tcNky0a9qGo0WdM+Lrd60tps+nkaJCmktKBJjr99fLtEBU1ipWQ=="
"version": "1.3.576",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz",
"integrity": "sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew=="
},
"emoji-regex": {
"version": "8.0.0",
@ -5088,10 +5096,11 @@
}
},
"enzyme-to-json": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.5.0.tgz",
"integrity": "sha512-clusXRsiaQhG7+wtyc4t7MU8N3zCOgf4eY9+CeSenYzKlFST4lxerfOvnWd4SNaToKhkuba+w6m242YpQOS7eA==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz",
"integrity": "sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==",
"requires": {
"@types/cheerio": "^0.22.22",
"lodash": "^4.17.15",
"react-is": "^16.12.0"
}
@ -5105,20 +5114,20 @@
}
},
"es-abstract": {
"version": "1.18.0-next.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz",
"integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==",
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"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-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -5894,19 +5903,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -6404,20 +6413,20 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true,
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -9391,32 +9400,12 @@
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
},
"object-is": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
"integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz",
"integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==",
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
},
"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"
}
}
"es-abstract": "^1.18.0-next.1"
}
},
"object-keys": {
@ -9454,19 +9443,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -9491,19 +9480,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -9520,19 +9509,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -9559,19 +9548,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -10217,20 +10206,20 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true,
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -10976,20 +10965,20 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"dev": true,
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -11015,19 +11004,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -11044,19 +11033,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
@ -11566,19 +11555,19 @@
},
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"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",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.0",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}