Implemented the factory & repository for simple products

This commit is contained in:
Christopher Allford 2020-09-17 13:31:39 -07:00
parent 6c230ca7b3
commit a9ee9806a4
14 changed files with 234 additions and 86 deletions

View File

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

View File

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

View File

@ -1,12 +0,0 @@
import { DeepPartial } from 'fishery';
import { Product } from './product';
/**
* The simple product class.
*/
export class SimpleProduct extends Product {
public constructor( partial: DeepPartial< SimpleProduct > = {} ) {
super( partial );
Object.assign( this, partial );
}
}

View File

@ -0,0 +1,47 @@
import { Model } from '../../models/model';
import { AsyncFactory } from '../async-factory';
class DummyModel extends Model {
public name: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
describe( 'AsyncFactory', () => {
let factory: AsyncFactory< DummyModel >;
beforeEach( () => {
let sequence = 1;
factory = new AsyncFactory< DummyModel >(
( { params } ) => {
const model = new DummyModel();
model.name = params.name ?? '';
return model;
},
( model ) => {
return Promise.resolve( new DummyModel( { id: sequence++, name: model.name } ) );
},
);
} );
it( 'should create', async () => {
const model = await factory.create( { name: 'test-name' } );
expect( model ).toHaveProperty( 'id', 1 );
expect( model ).toHaveProperty( 'name', 'test-name' );
} );
it( 'should create many', async () => {
const models = await factory.createList( 2, { name: 'test-name' } );
expect( models ).toHaveLength( 2 );
expect( models[ 0 ] ).toHaveProperty( 'id', 1 );
expect( models[ 0 ] ).toHaveProperty( 'name', 'test-name' );
expect( models[ 1 ] ).toHaveProperty( 'id', 2 );
expect( models[ 1 ] ).toHaveProperty( 'name', 'test-name' );
} );
} );

View File

@ -4,11 +4,6 @@ import Mock = jest.Mock;
class DummyModel extends Model {
public name: string = '';
public onCreated( data: any ): void {
super.onCreated( data );
this.name = data.name;
}
}
describe( 'ModelRepository', () => {
@ -24,28 +19,28 @@ describe( 'ModelRepository', () => {
} );
it( 'should create', async () => {
mockCallback.mockReturnValue( Promise.resolve( dummyModel ) );
mockCallback.mockResolvedValue( dummyModel );
await repository.create( dummyModel );
expect( mockCallback ).toHaveBeenCalledWith( dummyModel );
} );
it( 'should read', async () => {
mockCallback.mockReturnValue( Promise.resolve( dummyModel ) );
mockCallback.mockResolvedValue( dummyModel );
await repository.read( { id: 'test' } );
expect( mockCallback ).toHaveBeenCalledWith( { id: 'test' } );
await repository.read( { id: 1 } );
expect( mockCallback ).toHaveBeenCalledWith( { id: 1 } );
} );
it( 'should update', async () => {
mockCallback.mockReturnValue( Promise.resolve( dummyModel ) );
mockCallback.mockResolvedValue( dummyModel );
await repository.update( dummyModel );
expect( mockCallback ).toHaveBeenCalledWith( dummyModel );
} );
it( 'should delete', async () => {
mockCallback.mockReturnValue( Promise.resolve( true ) );
mockCallback.mockResolvedValue( true );
await repository.delete( dummyModel );
expect( mockCallback ).toHaveBeenCalledWith( dummyModel );

View File

@ -1,25 +1,18 @@
import { DeepPartial, Factory as BaseFactory, BuildOptions } from 'fishery';
import { Repository } from './repository';
import { BuildOptions, DeepPartial, Factory } from 'fishery';
import { GeneratorFnOptions } from 'fishery/dist/types';
import { Model } from '../models/model';
/**
* A factory that can be used to create models using an adapter.
*/
export class RepositoryFactory< T extends Model, I = any > extends BaseFactory< T, I > {
private repository: Repository< T > | null = null;
public constructor( generator: ( opts: GeneratorFnOptions< T, I > ) => T ) {
super( generator );
}
export class AsyncFactory< T, I = any > extends Factory< T, I > {
private readonly creator: ( model: T ) => Promise< T >;
/**
* Sets the repository that the factory should use when creating data.
* Creates a new factory instance.
*
* @param {Repository|null} repository The repository to set.
* @param {Function} generator The factory's generator function.
* @param {Function} creator The factory's creation function.
*/
public setRepository( repository: Repository< T > | null ): void {
this.repository = repository;
public constructor( generator: ( opts: GeneratorFnOptions< T, I > ) => T, creator: ( model: T ) => Promise< T > ) {
super( generator );
this.creator = creator;
}
/**
@ -30,12 +23,8 @@ export class RepositoryFactory< T extends Model, I = any > extends BaseFactory<
* @return {Promise} Resolves to the created model.
*/
public create( params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T > {
if ( ! this.repository ) {
throw new Error( 'The factory has no repository to create using.' );
}
const model = this.build( params, options );
return this.repository.create( model );
return this.creator( model );
}
/**
@ -47,14 +36,10 @@ export class RepositoryFactory< T extends Model, I = any > extends BaseFactory<
* @return {Promise} Resolves to the created models.
*/
public createList( number: number, params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T[] > {
if ( ! this.repository ) {
throw new Error( 'The factory has no repository to create using.' );
}
const models = this.buildList( number, params, options );
const promises: Promise< T >[] = [];
for ( const model of models ) {
promises.push( this.repository.create( model ) );
promises.push( this.create( model ) );
}
return Promise.all( promises );

View File

@ -9,7 +9,7 @@ type DeleteFn< T > = ( model: T ) => Promise< boolean >;
* The standard parameters for reading a model.
*/
interface DefaultReadParams {
id: string;
id: number;
}
/**

View File

@ -30,7 +30,7 @@ export class AxiosClient implements HTTPClient {
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
public get< T >(
public get< T = any >(
path: string,
params?: any,
): Promise< HTTPResponse< T >> {
@ -44,7 +44,7 @@ export class AxiosClient implements HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
public post< T >(
public post< T = any >(
path: string,
data?: any,
): Promise< HTTPResponse< T >> {
@ -58,7 +58,7 @@ export class AxiosClient implements HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
public put< T >(
public put< T = any >(
path: string,
data?: any,
): Promise< HTTPResponse< T >> {
@ -72,7 +72,7 @@ export class AxiosClient implements HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
public patch< T >(
public patch< T = any >(
path: string,
data?: any,
): Promise< HTTPResponse< T >> {
@ -86,7 +86,7 @@ export class AxiosClient implements HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
public delete< T >(
public delete< T = any >(
path: string,
data?: any,
): Promise< HTTPResponse< T >> {

View File

@ -31,7 +31,7 @@ export interface HTTPClient {
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
get< T >( path: string, params?: any ): Promise< HTTPResponse< T > >;
get< T = any >( path: string, params?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a POST request.
@ -40,7 +40,7 @@ export interface HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
post< T >( path: string, data?: any ): Promise< HTTPResponse< T > >;
post< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a PUT request.
@ -49,7 +49,7 @@ export interface HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
put< T >( path: string, data?: any ): Promise< HTTPResponse< T > >;
put< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a PATCH request.
@ -58,7 +58,7 @@ export interface HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
patch< T >( path: string, data?: any ): Promise< HTTPResponse< T > >;
patch< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a DELETE request.
@ -67,5 +67,5 @@ export interface HTTPClient {
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an HTTPResponse.
*/
delete< T >( path: string, data?: any ): Promise< HTTPResponse< T > >;
delete< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
}

View File

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

View File

@ -2,13 +2,5 @@
* A base class for all models.
*/
export abstract class Model {
private _id: number | null = null;
public get id(): number | null {
return this._id;
}
public onCreated( data: any ): void {
this._id = data.id;
}
public readonly id: number | null = null;
}

View File

@ -0,0 +1,68 @@
import { SimpleProduct } from '../simple-product';
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../http';
import { ModelRepository } from '../../../framework/model-repository';
describe( 'SimpleProduct', () => {
describe( 'restRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ModelRepository< SimpleProduct >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = SimpleProduct.restRepository( httpClient );
} );
it( 'should create', async () => {
httpClient.post.mockResolvedValue( new HTTPResponse( 200, {}, { id: 2 } ) );
const created = await repository.create( new SimpleProduct( { name: 'test' } ) );
expect( created ).toHaveProperty( 'id', 2 );
expect( httpClient.post ).toHaveBeenCalledWith(
'/wc/v3/products',
{
name: 'test',
},
);
} );
it( 'should read', async () => {
httpClient.get.mockResolvedValue(
new HTTPResponse( 200, {}, {
id: 12,
name: 'test-name',
} ),
);
const read = await repository.read( { id: 12 } );
expect( read ).toHaveProperty( 'id', 12 );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/products/12' );
} );
it( 'should update', async () => {
httpClient.put.mockResolvedValue( new HTTPResponse( 200, {}, { id: 1 } ) );
const updated = await repository.update( new SimpleProduct( { id: 1, name: 'test' } ) );
expect( updated ).toHaveProperty( 'id', 1 );
expect( httpClient.put ).toHaveBeenCalledWith(
'/wc/v3/products/1',
{
id: 1,
name: 'test',
},
);
} );
it( 'should delete', async () => {
httpClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const response = await repository.delete( new SimpleProduct( { id: 123 } ) );
expect( response ).toBeTruthy();
expect( httpClient.delete ).toHaveBeenCalledWith( '/wc/v3/products/123' );
} );
} );
} );

View File

@ -0,0 +1,9 @@
import { Model } from '../model';
/**
* The base class for all product types.
*/
export abstract class AbstractProduct extends Model {
public readonly name: string = '';
public readonly regularPrice: string = '';
}

View File

@ -0,0 +1,80 @@
import { AbstractProduct } from './abstract-product';
import * as faker from 'faker/locale/en';
import { HTTPClient } from '../../http';
import { ModelRepository } from '../../framework/model-repository';
import { AsyncFactory } from '../../framework/async-factory';
/**
* The simple product class.
*/
export class SimpleProduct extends AbstractProduct {
public constructor( partial: Partial< SimpleProduct > = {} ) {
super();
Object.assign( this, partial );
}
/**
* Creates a model repository configured for communicating via the REST API.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
* @return {ModelRepository} The created repository.
*/
public static restRepository( httpClient: HTTPClient ): ModelRepository< SimpleProduct > {
return new ModelRepository(
async ( model ) => {
const response = await httpClient.post(
'/wc/v3/products',
{
name: model.name,
},
);
return Promise.resolve( new SimpleProduct( {
id: response.data.id,
name: response.data.name,
} ) );
},
async ( params ) => {
const response = await httpClient.get( '/wc/v3/products/' + params.id );
const model = new SimpleProduct(
{
id: response.data.id,
name: response.data.name,
},
);
return Promise.resolve( model );
},
async ( model ) => {
return httpClient.put(
'/wc/v3/products/' + model.id,
{
id: model.id,
name: model.name,
},
).then( () => model );
},
async ( model ) => {
return httpClient.delete( '/wc/v3/products/' + model.id ).then( () => true );
},
);
}
/**
* Creates a new factory instance.
*
* @param {ModelRepository} repository The repository to use for creation.
* @return {AsyncFactory} The new factory instance.
*/
public static factory( repository: ModelRepository< SimpleProduct > ): AsyncFactory< SimpleProduct > {
return new AsyncFactory< SimpleProduct >(
( { params } ) => {
return new SimpleProduct( {
name: params.name ?? faker.commerce.productName(),
regularPrice: params.regularPrice ?? faker.commerce.price(),
} );
},
repository.create,
);
}
}