Refactored the factory to use a repository instead of an adapter so that we can expose more API functionality
This commit is contained in:
parent
a875ecb083
commit
39c5bc6b74
|
@ -10,11 +10,23 @@ module.exports = {
|
|||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'no-dupe-class-members': 'off',
|
||||
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 2,
|
||||
},
|
||||
'plugins': [
|
||||
'@typescript-eslint'
|
||||
],
|
||||
extends: [
|
||||
'plugin:@wordpress/eslint-plugin/recommended-with-formatting'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
'files': [
|
||||
'**/*.js',
|
||||
'**/*.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
'files': [
|
||||
'**/*.spec.ts',
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { Model } from './model';
|
||||
|
||||
/**
|
||||
* An interface for implementing adapters to create models.
|
||||
*/
|
||||
export interface Adapter< T extends Model > {
|
||||
/**
|
||||
* Creates a model or array of models using a service..
|
||||
*
|
||||
* @param {Model|Model[]} model The model or array of models to create.
|
||||
* @return {Promise} Resolves to the created input model or array of models.
|
||||
*/
|
||||
create( model: T ): Promise< T >;
|
||||
create( model: T[] ): Promise< T[]>;
|
||||
create( model: T | T[] ): Promise< T > | Promise< T[]>;
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import { Model } from '../model';
|
||||
import { APIAdapter } from './api-adapter';
|
||||
import { SimpleProduct } from '../../models/simple-product';
|
||||
import { APIResponse, APIService } from './api-service';
|
||||
|
||||
class MockAPI implements APIService {
|
||||
public get = jest.fn();
|
||||
public post = jest.fn();
|
||||
public put = jest.fn();
|
||||
public patch = jest.fn();
|
||||
public delete = jest.fn();
|
||||
}
|
||||
|
||||
describe( 'APIModelCreator', () => {
|
||||
let adapter: APIAdapter< Model >;
|
||||
let mockService: MockAPI;
|
||||
|
||||
beforeEach( () => {
|
||||
adapter = new APIAdapter( '/wc/v3/product', () => 'test' );
|
||||
mockService = new MockAPI();
|
||||
adapter.setAPIService( mockService );
|
||||
} );
|
||||
|
||||
it( 'should create single instance', async () => {
|
||||
mockService.post.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) );
|
||||
|
||||
const result = await adapter.create( new SimpleProduct() );
|
||||
|
||||
expect( result ).toBeInstanceOf( SimpleProduct );
|
||||
expect( result.id ).toBe( 1 );
|
||||
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
|
||||
} );
|
||||
|
||||
it( 'should create multiple instances', async () => {
|
||||
mockService.post
|
||||
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) )
|
||||
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 2 } ) ) )
|
||||
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 3 } ) ) );
|
||||
|
||||
const result = await adapter.create( [ new SimpleProduct(), new SimpleProduct(), new SimpleProduct() ] );
|
||||
|
||||
expect( result ).toBeInstanceOf( Array );
|
||||
expect( result ).toHaveLength( 3 );
|
||||
expect( result[ 0 ].id ).toBe( 1 );
|
||||
expect( result[ 1 ].id ).toBe( 2 );
|
||||
expect( result[ 2 ].id ).toBe( 3 );
|
||||
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
|
||||
expect( mockService.post.mock.calls[ 1 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||
expect( mockService.post.mock.calls[ 1 ][ 1 ] ).toBe( 'test' );
|
||||
expect( mockService.post.mock.calls[ 2 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||
expect( mockService.post.mock.calls[ 2 ][ 1 ] ).toBe( 'test' );
|
||||
} );
|
||||
} );
|
|
@ -1,87 +0,0 @@
|
|||
import { APIResponse, APIService } from './api-service';
|
||||
import { Model } from '../model';
|
||||
import { Adapter } from '../adapter';
|
||||
|
||||
/**
|
||||
* A callback for transforming models into an API request body.
|
||||
*
|
||||
* @callback APITransformerFn
|
||||
* @param {Model} model The model that we want to transform.
|
||||
* @return {*} The structured request data for the API.
|
||||
*/
|
||||
export type APITransformerFn< T extends Model > = ( model: T ) => any;
|
||||
|
||||
/**
|
||||
* A class used for creating data models using a supplied API endpoint.
|
||||
*/
|
||||
export class APIAdapter< T extends Model > implements Adapter< T > {
|
||||
private readonly endpoint: string;
|
||||
private readonly transformer: APITransformerFn< T >;
|
||||
private apiService: APIService | null;
|
||||
|
||||
public constructor( endpoint: string, transformer: APITransformerFn< T > ) {
|
||||
this.endpoint = endpoint;
|
||||
this.transformer = transformer;
|
||||
this.apiService = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API service that the adapter should use for creation actions.
|
||||
*
|
||||
* @param {APIService|null} service The new API service for the adapter to use.
|
||||
*/
|
||||
public setAPIService( service: APIService | null ): void {
|
||||
this.apiService = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a model or array of models using the API service.
|
||||
*
|
||||
* @param {Model|Model[]} model The model or array of models to create.
|
||||
* @return {Promise} Resolves to the created input model or array of models.
|
||||
*/
|
||||
public create( model: T ): Promise< T >;
|
||||
public create( model: T[] ): Promise< T[]>;
|
||||
public create( model: T | T[] ): Promise< T > | Promise< T[]> {
|
||||
if ( ! this.apiService ) {
|
||||
throw new Error( 'An API service must be registered for the adapter to work.' );
|
||||
}
|
||||
|
||||
if ( Array.isArray( model ) ) {
|
||||
return this.createList( model );
|
||||
}
|
||||
|
||||
return this.createSingle( model );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single model using the API service.
|
||||
*
|
||||
* @param {Model} model The model to create.
|
||||
* @return {Promise} Resolves to the created input model.
|
||||
*/
|
||||
private async createSingle( model: T ): Promise< T > {
|
||||
return this.apiService!.post(
|
||||
this.endpoint,
|
||||
this.transformer( model ),
|
||||
).then( ( data: APIResponse ) => {
|
||||
model.setID( data.data.id );
|
||||
return model;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of models using the API service.
|
||||
*
|
||||
* @param {Model[]} models The array of models to create.
|
||||
* @return {Promise} Resolves to the array of created input models.
|
||||
*/
|
||||
private async createList( models: T[] ): Promise< T[]> {
|
||||
const promises: Promise< T >[] = [];
|
||||
for ( const model of models ) {
|
||||
promises.push( this.createSingle( model ) );
|
||||
}
|
||||
|
||||
return Promise.all( promises );
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/**
|
||||
* A structured response from the API.
|
||||
*/
|
||||
export class APIResponse< T = any > {
|
||||
public readonly status: number;
|
||||
public readonly headers: any;
|
||||
public readonly data: T;
|
||||
|
||||
public constructor( status: number, headers: any, data: T ) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structured error from the API.
|
||||
*/
|
||||
export class APIError {
|
||||
public readonly code: string;
|
||||
public readonly message: string;
|
||||
public readonly data: any;
|
||||
|
||||
public constructor( code: string, message: string, data: any ) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not an APIResponse contains an error.
|
||||
*
|
||||
* @param {APIResponse} response The response to evaluate.
|
||||
*/
|
||||
export function isAPIError( response: APIResponse ): response is APIResponse< APIError > {
|
||||
return response.status < 200 || response.status >= 400;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for implementing services to make calls against the API.
|
||||
*/
|
||||
export interface APIService {
|
||||
/**
|
||||
* Performs a GET request against the WordPress API.
|
||||
*
|
||||
* @param {string} endpoint The API endpoint we should query.
|
||||
* @param {*} params Any parameters that should be passed in the request.
|
||||
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||
*/
|
||||
get< T >(
|
||||
endpoint: string,
|
||||
params?: any
|
||||
): Promise< APIResponse< T >>;
|
||||
|
||||
/**
|
||||
* Performs a POST request against the WordPress API.
|
||||
*
|
||||
* @param {string} endpoint The API endpoint we should query.
|
||||
* @param {*} data Any parameters that should be passed in the request.
|
||||
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||
*/
|
||||
post< T >(
|
||||
endpoint: string,
|
||||
data?: any
|
||||
): Promise< APIResponse< T >>;
|
||||
|
||||
/**
|
||||
* Performs a PUT request against the WordPress API.
|
||||
*
|
||||
* @param {string} endpoint The API endpoint we should query.
|
||||
* @param {*} data Any parameters that should be passed in the request.
|
||||
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||
*/
|
||||
put< T >( endpoint: string, data?: any ): Promise< APIResponse< T >>;
|
||||
|
||||
/**
|
||||
* Performs a PATCH request against the WordPress API.
|
||||
*
|
||||
* @param {string} endpoint The API endpoint we should query.
|
||||
* @param {*} data Any parameters that should be passed in the request.
|
||||
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||
*/
|
||||
patch< T >(
|
||||
endpoint: string,
|
||||
data?: any
|
||||
): Promise< APIResponse< T >>;
|
||||
|
||||
/**
|
||||
* Performs a DELETE request against the WordPress API.
|
||||
*
|
||||
* @param {string} endpoint The API endpoint we should query.
|
||||
* @param {*} data Any parameters that should be passed in the request.
|
||||
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||
*/
|
||||
delete< T >(
|
||||
endpoint: string,
|
||||
data?: any
|
||||
): Promise< APIResponse< T >>;
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import moxios from 'moxios';
|
||||
import { APIResponse } from '../api-service';
|
||||
import { AxiosAPIService } from './axios-api-service';
|
||||
|
||||
describe( 'AxiosAPIService', () => {
|
||||
let apiClient: AxiosAPIService;
|
||||
|
||||
beforeEach( () => {
|
||||
moxios.install();
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
moxios.uninstall();
|
||||
} );
|
||||
|
||||
it( 'should add OAuth interceptors', async () => {
|
||||
apiClient = AxiosAPIService.createUsingOAuth(
|
||||
'http://test.test/wp-json/',
|
||||
'consumer_key',
|
||||
'consumer_secret',
|
||||
);
|
||||
|
||||
moxios.stubOnce( 'GET', '/wc/v2/product', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseText: JSON.stringify( { test: 'value' } ),
|
||||
} );
|
||||
|
||||
const response = await apiClient.get( '/wc/v2/product' );
|
||||
expect( response ).toBeInstanceOf( APIResponse );
|
||||
|
||||
const request = moxios.requests.mostRecent();
|
||||
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||
expect( request.headers.Authorization ).toMatch( /^OAuth/ );
|
||||
} );
|
||||
|
||||
it( 'should add basic auth interceptors', async () => {
|
||||
apiClient = AxiosAPIService.createUsingBasicAuth( 'http://test.test/wp-json/', 'test', 'pass' );
|
||||
|
||||
moxios.stubOnce( 'GET', '/wc/v2/product', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseText: JSON.stringify( { test: 'value' } ),
|
||||
} );
|
||||
|
||||
const response = await apiClient.get( '/wc/v2/product' );
|
||||
expect( response ).toBeInstanceOf( APIResponse );
|
||||
|
||||
const request = moxios.requests.mostRecent();
|
||||
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||
expect( request.headers.Authorization ).toMatch( /^Basic/ );
|
||||
} );
|
||||
} );
|
|
@ -1,57 +0,0 @@
|
|||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { APIResponse, APIService } from '../api-service';
|
||||
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
|
||||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { AxiosResponseInterceptor } from './axios-response-interceptor';
|
||||
|
||||
/**
|
||||
* An API service implementation that uses Axios to make requests to the WordPress API.
|
||||
*/
|
||||
export class AxiosAPIService implements APIService {
|
||||
private readonly client: AxiosInstance;
|
||||
private readonly interceptors: AxiosInterceptor[];
|
||||
|
||||
public constructor( config: AxiosRequestConfig, interceptors: AxiosInterceptor[] = [] ) {
|
||||
this.client = axios.create( config );
|
||||
this.interceptors = interceptors;
|
||||
for ( const interceptor of this.interceptors ) {
|
||||
interceptor.start( this.client );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Axios API Service using OAuth 1.0a one-legged authentication.
|
||||
*
|
||||
* @param {string} apiURL The base URL for the API requests to be sent.
|
||||
* @param {string} consumerKey The OAuth consumer key.
|
||||
* @param {string} consumerSecret The OAuth consumer secret.
|
||||
* @return {AxiosAPIService} The created service.
|
||||
*/
|
||||
public static createUsingOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): AxiosAPIService {
|
||||
return new AxiosAPIService(
|
||||
{ baseURL: apiURL },
|
||||
[
|
||||
new AxiosOAuthInterceptor( consumerKey, consumerSecret ),
|
||||
new AxiosResponseInterceptor(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Axios API Service using basic authentication.
|
||||
*
|
||||
* @param {string} apiURL The base URL for the API requests to be sent.
|
||||
* @param {string} username The username for authentication.
|
||||
* @param {string} password The password for authentication.
|
||||
* @return {AxiosAPIService} The created service.
|
||||
*/
|
||||
public static createUsingBasicAuth( apiURL: string, username: string, password: string ): AxiosAPIService {
|
||||
return new AxiosAPIService(
|
||||
{
|
||||
baseURL: apiURL,
|
||||
auth: { username, password },
|
||||
},
|
||||
[ new AxiosResponseInterceptor() ],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* CORE CLASSES
|
||||
* These exports relate to extending the core functionality of the package.
|
||||
*/
|
||||
export { Adapter } from './adapter';
|
||||
export { ModelFactory } from './model-factory';
|
||||
export { Model } from './model';
|
||||
|
||||
/**
|
||||
* API ADAPTER
|
||||
* These exports relate to replacing the underlying HTTP layer of API adapters.
|
||||
*/
|
||||
export { APIAdapter } from './api/api-adapter';
|
||||
export { APIService, APIResponse, APIError } from './api/api-service';
|
|
@ -1,41 +0,0 @@
|
|||
import { ModelFactory } from './model-factory';
|
||||
import { Adapter } from './adapter';
|
||||
import { Product } from '../models/product';
|
||||
import { SimpleProduct } from '../models/simple-product';
|
||||
|
||||
class MockAdapter implements Adapter< Product > {
|
||||
public create = jest.fn();
|
||||
}
|
||||
|
||||
describe( 'ModelFactory', () => {
|
||||
let mockAdapter: MockAdapter;
|
||||
let factory: ModelFactory< Product >;
|
||||
|
||||
beforeEach( () => {
|
||||
mockAdapter = new MockAdapter();
|
||||
factory = ModelFactory.define< Product, any, ModelFactory< Product >>(
|
||||
( { params } ) => {
|
||||
return new SimpleProduct( params );
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should error without adapter', async () => {
|
||||
expect( () => factory.create() ).toThrowError( /no adapter/ );
|
||||
} );
|
||||
|
||||
it( 'should create using adapter', async () => {
|
||||
factory.setAdapter( mockAdapter );
|
||||
|
||||
const expectedModel = new SimpleProduct( { name: 'test2' } );
|
||||
expectedModel.setID( 1 );
|
||||
mockAdapter.create.mockReturnValueOnce( Promise.resolve( expectedModel ) );
|
||||
|
||||
const created = await factory.create( { name: 'test' } );
|
||||
|
||||
expect( mockAdapter.create.mock.calls ).toHaveLength( 1 );
|
||||
expect( created ).toBeInstanceOf( Product );
|
||||
expect( created.id ).toBe( 1 );
|
||||
expect( created.name ).toBe( 'test2' );
|
||||
} );
|
||||
} );
|
|
@ -1,52 +0,0 @@
|
|||
import { DeepPartial, Factory, BuildOptions } from 'fishery';
|
||||
import { Model } from './model';
|
||||
import { Adapter } from './adapter';
|
||||
|
||||
/**
|
||||
* A factory that can be used to create models using an adapter.
|
||||
*/
|
||||
export class ModelFactory< T extends Model, I = any > extends Factory< T, I > {
|
||||
private adapter: Adapter< T > | null = null;
|
||||
|
||||
/**
|
||||
* Sets the adapter that the factory will use to create models.
|
||||
*
|
||||
* @param {Adapter|null} adapter
|
||||
*/
|
||||
public setAdapter( adapter: Adapter< T > | null ): void {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an object using your factory
|
||||
*
|
||||
* @param {DeepPartial} params The parameters that should populate the object.
|
||||
* @param {BuildOptions} options The options to be used in the builder.
|
||||
* @return {Promise} Resolves to the created model.
|
||||
*/
|
||||
public create( params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T > {
|
||||
if ( ! this.adapter ) {
|
||||
throw new Error( 'The factory has no adapter to create using.' );
|
||||
}
|
||||
|
||||
const model = this.build( params, options );
|
||||
return this.adapter.create( model );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of objects using your factory
|
||||
*
|
||||
* @param {number} number The number of models to create.
|
||||
* @param {DeepPartial} params The parameters that should populate the object.
|
||||
* @param {BuildOptions} options The options to be used in the builder.
|
||||
* @return {Promise} Resolves to the created model.
|
||||
*/
|
||||
public createList( number: number, params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T[]> {
|
||||
if ( ! this.adapter ) {
|
||||
throw new Error( 'The factory has no adapter to create using.' );
|
||||
}
|
||||
|
||||
const model = this.buildList( number, params, options );
|
||||
return this.adapter.create( model );
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import { DeepPartial } from 'fishery';
|
||||
|
||||
/**
|
||||
* A base class for all models.
|
||||
*/
|
||||
export abstract class Model {
|
||||
private _id: number = 0;
|
||||
|
||||
protected constructor( partial: DeepPartial< any > = {} ) {
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
|
||||
public get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public setID( id: number ): void {
|
||||
this._id = id;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Model } from '../framework/model';
|
||||
import { Model } from './model';
|
||||
import { DeepPartial } from 'fishery';
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,48 +1,12 @@
|
|||
import { DeepPartial } from 'fishery';
|
||||
import { Product } from './product';
|
||||
import { AdapterTypes, ModelRegistry } from '../framework/model-registry';
|
||||
import { ModelFactory } from '../framework/model-factory';
|
||||
import { APIAdapter } from '../framework/api/api-adapter';
|
||||
import faker from 'faker/locale/en';
|
||||
|
||||
/**
|
||||
* The simple product class.
|
||||
*/
|
||||
export class SimpleProduct extends Product {
|
||||
public constructor( partial: DeepPartial< SimpleProduct > = {} ) {
|
||||
super( partial );
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the simple product factory and adapters.
|
||||
*
|
||||
* @param {ModelRegistry} registry The registry to hold the model reference.
|
||||
*/
|
||||
export function registerSimpleProduct( registry: ModelRegistry ): void {
|
||||
if ( null !== registry.getFactory( SimpleProduct ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const factory = ModelFactory.define< SimpleProduct, any, ModelFactory< SimpleProduct >>(
|
||||
( { params } ) => {
|
||||
return new SimpleProduct(
|
||||
{
|
||||
name: params.name ?? faker.commerce.productName(),
|
||||
regularPrice: params.regularPrice ?? faker.commerce.price(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
registry.registerFactory( SimpleProduct, factory );
|
||||
|
||||
const apiAdapter = new APIAdapter< SimpleProduct >(
|
||||
'/wc/v3/products',
|
||||
( model ) => {
|
||||
return {
|
||||
type: 'simple',
|
||||
name: model.name,
|
||||
regular_price: model.regularPrice,
|
||||
};
|
||||
},
|
||||
);
|
||||
registry.registerAdapter( SimpleProduct, AdapterTypes.API, apiAdapter );
|
||||
}
|
||||
|
|
|
@ -2778,6 +2778,15 @@
|
|||
"@jest/types": "^25.5.0"
|
||||
}
|
||||
},
|
||||
"jest-mock-extended": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-1.0.10.tgz",
|
||||
"integrity": "sha512-R2wKiOgEUPoHZ2kLsAQeQP2IfVEgo3oQqWLSXKdMXK06t3UHkQirA2Xnsdqg/pX6KPWTsdnrzE2ig6nqNjdgVw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ts-essentials": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"jest-pnp-resolver": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||
|
@ -4607,6 +4616,12 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ts-essentials": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-4.0.0.tgz",
|
||||
"integrity": "sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-jest": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.0.tgz",
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"@types/moxios": "0.4.9",
|
||||
"@types/node": "13.13.5",
|
||||
"jest": "25.5.4",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"moxios": "0.4.0",
|
||||
"ts-jest": "25.5.0",
|
||||
"typescript": "3.8.3"
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { Repository, RepositoryData } from '../repository';
|
||||
import { RepositoryFactory } from '../repository-factory';
|
||||
|
||||
class TestData implements RepositoryData {
|
||||
public constructor( public name: string ) {}
|
||||
onCreated( data: any ): void {
|
||||
this.name = data.name;
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'RepositoryFactory', () => {
|
||||
let repository: MockProxy< Repository< TestData > >;
|
||||
|
||||
beforeEach( () => {
|
||||
repository = mock< Repository< TestData > >();
|
||||
} );
|
||||
|
||||
it( 'should error for create without repository', () => {
|
||||
const factory = new RepositoryFactory( () => new TestData( '' ) );
|
||||
|
||||
expect( () => factory.create() ).toThrowError();
|
||||
} );
|
||||
|
||||
it( 'should create using repository', async () => {
|
||||
const factory = new RepositoryFactory( () => new TestData( '' ) );
|
||||
factory.setRepository( repository );
|
||||
|
||||
repository.create.mockReturnValueOnce( Promise.resolve( new TestData( 'created' ) ) );
|
||||
|
||||
const created = await factory.create( { name: 'test' } );
|
||||
|
||||
expect( created ).toBeInstanceOf( TestData );
|
||||
expect( created.name ).toEqual( 'created' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,43 @@
|
|||
import { HTTPClient, HTTPResponse } from '../../http';
|
||||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { RESTRepository } from '../rest-repository';
|
||||
import { RepositoryData } from '../repository';
|
||||
|
||||
class TestData implements RepositoryData {
|
||||
public constructor( public name: string ) {}
|
||||
onCreated( data: any ): void {
|
||||
this.name = data.name;
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'RESTRepository', () => {
|
||||
let httpClient: MockProxy< HTTPClient >;
|
||||
let repository: RESTRepository< TestData >;
|
||||
|
||||
beforeEach( () => {
|
||||
httpClient = mock< HTTPClient >();
|
||||
repository = new RESTRepository< TestData >(
|
||||
( data ) => {
|
||||
return { transformedName: data.name };
|
||||
},
|
||||
{ create: '/testing' },
|
||||
);
|
||||
repository.setHTTPClient( httpClient );
|
||||
} );
|
||||
|
||||
it( 'should create', async () => {
|
||||
const model = new TestData( 'testing' );
|
||||
|
||||
httpClient.post.mockReturnValueOnce(
|
||||
Promise.resolve(
|
||||
new HTTPResponse( 200, {}, { id: 1, name: 'created' } ),
|
||||
),
|
||||
);
|
||||
|
||||
const created = await repository.create( model );
|
||||
|
||||
expect( created.name ).toEqual( 'created' );
|
||||
expect( httpClient.post.mock.calls[ 0 ][ 0 ] ).toEqual( '/testing' );
|
||||
expect( httpClient.post.mock.calls[ 0 ][ 1 ] ).toEqual( { transformedName: 'testing' } );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,61 @@
|
|||
import { DeepPartial, Factory as BaseFactory, BuildOptions } from 'fishery';
|
||||
import { Repository, RepositoryData } from './repository';
|
||||
import { GeneratorFnOptions } from 'fishery/dist/types';
|
||||
|
||||
/**
|
||||
* A factory that can be used to create models using an adapter.
|
||||
*/
|
||||
export class RepositoryFactory< T extends RepositoryData, I = any > extends BaseFactory< T, I > {
|
||||
private repository: Repository< T > | null = null;
|
||||
|
||||
public constructor( generator: ( opts: GeneratorFnOptions< T, I > ) => T ) {
|
||||
super( generator );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the repository that the factory should use when creating data.
|
||||
*
|
||||
* @param {Repository|null} repository The repository to set.
|
||||
*/
|
||||
public setRepository( repository: Repository< T > | null ): void {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an object using your factory
|
||||
*
|
||||
* @param {DeepPartial} params The parameters that should populate the object.
|
||||
* @param {BuildOptions} options The options to be used in the builder.
|
||||
* @return {Promise} Resolves to the created model.
|
||||
*/
|
||||
public create( params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T > {
|
||||
if ( ! this.repository ) {
|
||||
throw new Error( 'The factory has no repository to create using.' );
|
||||
}
|
||||
|
||||
const model = this.build( params, options );
|
||||
return this.repository.create( model );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of objects using your factory
|
||||
*
|
||||
* @param {number} number The number of models to create.
|
||||
* @param {DeepPartial} params The parameters that should populate the object.
|
||||
* @param {BuildOptions} options The options to be used in the builder.
|
||||
* @return {Promise} Resolves to the created 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 ) );
|
||||
}
|
||||
|
||||
return Promise.all( promises );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* A function that should transform data into a format to be consumed in a repository.
|
||||
*/
|
||||
export type TransformFn< T > = ( data: T ) => any;
|
||||
|
||||
/**
|
||||
* An interface for data that repositories interact with.
|
||||
*/
|
||||
export interface RepositoryData {
|
||||
/**
|
||||
* Marks that the model was created.
|
||||
*
|
||||
* @param {*} data The data from the repository.
|
||||
*/
|
||||
onCreated( data: any ): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for repositories that perform CRUD actions.
|
||||
*/
|
||||
export interface Repository< T extends RepositoryData > {
|
||||
/**
|
||||
* Uses the repository to create the given data.
|
||||
*
|
||||
* @param {*} data The data that we would like to create.
|
||||
* @return {Promise} A promise that resolves to the data after creation.
|
||||
*/
|
||||
create( data: T ): Promise< T >;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { Repository, RepositoryData, TransformFn } from './repository';
|
||||
import { HTTPClient, HTTPResponse } from '../http';
|
||||
|
||||
/**
|
||||
* An interface for describing the endpoints available to the repository.
|
||||
*
|
||||
* @typedef {Object} Endpoints
|
||||
* @property {string} create The creation endpoint.
|
||||
*/
|
||||
export interface Endpoints {
|
||||
create?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A repository for interacting with models via the REST API.
|
||||
*/
|
||||
export class RESTRepository< T extends RepositoryData > implements Repository< T > {
|
||||
private httpClient: HTTPClient | null = null;
|
||||
private readonly transformer: TransformFn< T >;
|
||||
private readonly endpoints: Endpoints;
|
||||
|
||||
public constructor(
|
||||
transformer: TransformFn< T >,
|
||||
endpoints: Endpoints,
|
||||
) {
|
||||
this.transformer = transformer;
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP client that the repository should use for requests.
|
||||
*
|
||||
* @param {HTTPClient} httpClient The client to use.
|
||||
*/
|
||||
public setHTTPClient( httpClient: HTTPClient | null ): void {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the data using the REST API.
|
||||
*
|
||||
* @param {*} data The model that we would like to create.
|
||||
* @return {Promise} A promise that resolves to the model after creation.
|
||||
*/
|
||||
public async create( data: T ): Promise< T > {
|
||||
if ( ! this.httpClient ) {
|
||||
throw new Error( 'There is no HTTP client set to make the request.' );
|
||||
}
|
||||
if ( ! this.endpoints.create ) {
|
||||
throw new Error( 'There is no `create` endpoint defined.' );
|
||||
}
|
||||
|
||||
const endpoint = this.endpoints.create.replace( /{[a-z]+}/i, this.replaceEndpointTokens( data ) );
|
||||
const transformed = this.transformer( data );
|
||||
|
||||
return this.httpClient.post( endpoint, transformed )
|
||||
.then( ( response: HTTPResponse ) => {
|
||||
if ( response.status >= 400 ) {
|
||||
throw new Error( 'An error has occurred!' );
|
||||
}
|
||||
|
||||
data.onCreated( response.data );
|
||||
|
||||
return Promise.resolve( data );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the tokens in the endpoint according to properties of the data argument.
|
||||
*
|
||||
* @param {any} data The model to replace tokens using.
|
||||
*/
|
||||
private replaceEndpointTokens( data: any ): ( match: string ) => string {
|
||||
return ( match: string ) => data[ match ];
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ export class HTTPResponse< T = any > {
|
|||
}
|
||||
|
||||
/**
|
||||
* An interface for implementing clients for making HTTP requests..
|
||||
* An interface for clients that make HTTP requests.
|
||||
*/
|
||||
export interface HTTPClient {
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export { HTTPClient, HTTPResponse } from './http-client';
|
||||
|
||||
export { AxiosClient } from './axios/axios-client';
|
||||
export { AxiosInterceptor } from './axios/axios-interceptor';
|
||||
export { AxiosOAuthInterceptor } from './axios/axios-oauth-interceptor';
|
||||
export { AxiosResponseInterceptor } from './axios/axios-response-interceptor';
|
|
@ -0,0 +1,16 @@
|
|||
import { RepositoryData } from '../framework/repository';
|
||||
|
||||
/**
|
||||
* A base class for all models.
|
||||
*/
|
||||
export class Model implements RepositoryData {
|
||||
private _id: number | null = null;
|
||||
|
||||
public get id(): number | null {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public onCreated( data: any ): void {
|
||||
this._id = data.id;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"references": [
|
||||
{ "path": "tests/e2e/factories" }
|
||||
{ "path": "tests/e2e/api" }
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue