Centralized the CRUD actions into a single kind of repository that can be easily used in every adapter case
This commit is contained in:
parent
3a7c96b7cd
commit
6c230ca7b3
|
@ -0,0 +1,53 @@
|
|||
import { Model } from '../../models/model';
|
||||
import { ModelRepository } from '../model-repository';
|
||||
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', () => {
|
||||
let dummyModel: DummyModel;
|
||||
let mockCallback: Mock;
|
||||
let repository: ModelRepository< DummyModel >;
|
||||
|
||||
beforeEach( () => {
|
||||
dummyModel = new DummyModel();
|
||||
dummyModel.name = 'test';
|
||||
mockCallback = jest.fn();
|
||||
repository = new ModelRepository< DummyModel >( mockCallback, mockCallback, mockCallback, mockCallback );
|
||||
} );
|
||||
|
||||
it( 'should create', async () => {
|
||||
mockCallback.mockReturnValue( Promise.resolve( dummyModel ) );
|
||||
|
||||
await repository.create( dummyModel );
|
||||
expect( mockCallback ).toHaveBeenCalledWith( dummyModel );
|
||||
} );
|
||||
|
||||
it( 'should read', async () => {
|
||||
mockCallback.mockReturnValue( Promise.resolve( dummyModel ) );
|
||||
|
||||
await repository.read( { id: 'test' } );
|
||||
expect( mockCallback ).toHaveBeenCalledWith( { id: 'test' } );
|
||||
} );
|
||||
|
||||
it( 'should update', async () => {
|
||||
mockCallback.mockReturnValue( Promise.resolve( dummyModel ) );
|
||||
|
||||
await repository.update( dummyModel );
|
||||
expect( mockCallback ).toHaveBeenCalledWith( dummyModel );
|
||||
} );
|
||||
|
||||
it( 'should delete', async () => {
|
||||
mockCallback.mockReturnValue( Promise.resolve( true ) );
|
||||
|
||||
await repository.delete( dummyModel );
|
||||
expect( mockCallback ).toHaveBeenCalledWith( dummyModel );
|
||||
} );
|
||||
} );
|
|
@ -1,44 +0,0 @@
|
|||
import { Model } from '../../models/model';
|
||||
import { ModelTransformer } from '../model-transformer';
|
||||
|
||||
class TestData extends Model {
|
||||
public name: string = '';
|
||||
|
||||
public onCreated( data: any ): void {
|
||||
super.onCreated( data );
|
||||
this.name = data.name;
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'ModelTransformer', () => {
|
||||
let transformer: ModelTransformer< TestData >;
|
||||
|
||||
beforeEach( () => {
|
||||
transformer = new ModelTransformer<TestData>(
|
||||
( model ) => {
|
||||
return { id: model.id, name: model.name };
|
||||
},
|
||||
( data ) => {
|
||||
const model = new TestData();
|
||||
model.name = data.name;
|
||||
return model;
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should transform to server', () => {
|
||||
const model = new TestData();
|
||||
model.onCreated( { id: 1, name: 'Testing' } );
|
||||
|
||||
const transformed = transformer.toServer( model );
|
||||
|
||||
expect( transformed ).toEqual( { id: 1, name: 'Testing' } );
|
||||
} );
|
||||
|
||||
it( 'should transform from server', () => {
|
||||
const transformed = transformer.fromServer( { name: 'Testing' } );
|
||||
|
||||
expect( transformed ).toBeInstanceOf( TestData );
|
||||
expect( transformed.name ).toBe( 'Testing' );
|
||||
} );
|
||||
} );
|
|
@ -1,49 +0,0 @@
|
|||
import { HTTPClient, HTTPResponse } from '../../http';
|
||||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { RESTRepository } from '../rest-repository';
|
||||
import { Model } from '../../models/model';
|
||||
import { ModelTransformer } from '../model-transformer';
|
||||
|
||||
class TestData extends Model {
|
||||
public name: string = '';
|
||||
|
||||
public onCreated( data: any ): void {
|
||||
super.onCreated( data );
|
||||
this.name = data.name;
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'RESTRepository', () => {
|
||||
let httpClient: MockProxy< HTTPClient >;
|
||||
let repository: RESTRepository< TestData >;
|
||||
|
||||
beforeEach( () => {
|
||||
httpClient = mock< HTTPClient >();
|
||||
const transformer = new ModelTransformer< TestData >(
|
||||
( data ) => {
|
||||
return { transformedName: data.name };
|
||||
},
|
||||
() => new TestData(),
|
||||
);
|
||||
repository = new RESTRepository< TestData >(
|
||||
transformer,
|
||||
{ create: '/testing' },
|
||||
);
|
||||
repository.setHTTPClient( httpClient );
|
||||
} );
|
||||
|
||||
it( 'should create', async () => {
|
||||
httpClient.post.mockReturnValueOnce(
|
||||
Promise.resolve(
|
||||
new HTTPResponse( 200, {}, { id: 1, name: 'created' } ),
|
||||
),
|
||||
);
|
||||
|
||||
const data = new TestData();
|
||||
data.name = 'testing';
|
||||
const created = await repository.create( data );
|
||||
|
||||
expect( created.name ).toEqual( 'created' );
|
||||
expect( httpClient.post ).toBeCalledWith( '/testing', { transformedName: 'testing' } );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,83 @@
|
|||
import { Model } from '../models/model';
|
||||
|
||||
type CreateFn< T > = ( model: T ) => Promise< T >;
|
||||
type ReadFn< T, P > = ( params: P ) => Promise< T >;
|
||||
type UpdateFn< T > = ( model: T ) => Promise< T >;
|
||||
type DeleteFn< T > = ( model: T ) => Promise< boolean >;
|
||||
|
||||
/**
|
||||
* The standard parameters for reading a model.
|
||||
*/
|
||||
interface DefaultReadParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for repositories that perform CRUD actions.
|
||||
*/
|
||||
export class ModelRepository< T extends Model, ReadParams = DefaultReadParams > {
|
||||
private readonly createHook: CreateFn< T >;
|
||||
private readonly readHook: ReadFn< T, ReadParams >;
|
||||
private readonly updateHook: UpdateFn< T >;
|
||||
private readonly deleteHook: DeleteFn< T >;
|
||||
|
||||
/**
|
||||
* Creates a new repository instance.
|
||||
*
|
||||
* @param {Function} createHook The hook for model creation.
|
||||
* @param {Function} readHook The hook for model reading.
|
||||
* @param {Function} updateHook The hook for model updating.
|
||||
* @param {Function} deleteHook The hook for model deletion.
|
||||
*/
|
||||
public constructor(
|
||||
createHook: CreateFn< T >,
|
||||
readHook: ReadFn< T, ReadParams >,
|
||||
updateHook: UpdateFn< T >,
|
||||
deleteHook: DeleteFn< T >,
|
||||
) {
|
||||
this.createHook = createHook;
|
||||
this.readHook = readHook;
|
||||
this.updateHook = updateHook;
|
||||
this.deleteHook = deleteHook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the given model.
|
||||
*
|
||||
* @param {*} model The model that we would like to create.
|
||||
* @return {Promise} A promise that resolves to the model after creation.
|
||||
*/
|
||||
public create( model: T ): Promise< T > {
|
||||
return this.createHook( model );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the given model.
|
||||
*
|
||||
* @param {Object} params The parameters to help with reading the model.
|
||||
* @return {Promise} A promise that resolves to the model.
|
||||
*/
|
||||
public read( params: ReadParams ): Promise< T > {
|
||||
return this.readHook( params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given model.
|
||||
*
|
||||
* @param {*} model The model we want to update.
|
||||
* @return {Promise} A promise that resolves to the model after updating.
|
||||
*/
|
||||
public update( model: T ): Promise< T > {
|
||||
return this.updateHook( model );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given model.
|
||||
*
|
||||
* @param {*} model The model we want to delete.
|
||||
* @return {Promise} A promise that resolves to "true" on success.
|
||||
*/
|
||||
public delete( model: T ): Promise< boolean > {
|
||||
return this.deleteHook( model );
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { Model } from '../models/model';
|
||||
|
||||
type ToServerFn< T > = ( model: T ) => any;
|
||||
type FromServerFn< T > = ( data: any ) => T;
|
||||
|
||||
/**
|
||||
* A class for transforming models between the server representation and the client representation.
|
||||
*/
|
||||
export class ModelTransformer< T extends Model > {
|
||||
private readonly toServerHook: ToServerFn< T >;
|
||||
private readonly fromServerHook: FromServerFn< T >;
|
||||
|
||||
public constructor( toServer: ToServerFn< T >, fromServer: FromServerFn< T > ) {
|
||||
this.toServerHook = toServer;
|
||||
this.fromServerHook = fromServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the model to a representation the server will understand.
|
||||
*
|
||||
* @param {Model} model The model to transform.
|
||||
* @return {*} The transformed model.
|
||||
*/
|
||||
public toServer( model: T ): any {
|
||||
return this.toServerHook( model );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the model from the server's representation to the client model.
|
||||
*
|
||||
* @param {*} data The server representation.
|
||||
* @return {Model} The transformed model.
|
||||
*/
|
||||
public fromServer( data: any ): T {
|
||||
return this.fromServerHook( data );
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { Model } from '../models/model';
|
||||
|
||||
/**
|
||||
* An interface for repositories that perform CRUD actions.
|
||||
*/
|
||||
export interface Repository< T extends Model > {
|
||||
/**
|
||||
* 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 >;
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
import { Repository } from './repository';
|
||||
import { HTTPClient, HTTPResponse } from '../http';
|
||||
import { Model } from '../models/model';
|
||||
import { ModelTransformer } from './model-transformer';
|
||||
|
||||
/**
|
||||
* 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 Model > implements Repository< T > {
|
||||
private httpClient: HTTPClient | null = null;
|
||||
private readonly transformer: ModelTransformer< T >;
|
||||
private readonly endpoints: Endpoints;
|
||||
|
||||
public constructor(
|
||||
transformer: ModelTransformer< 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.toServer( 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 ];
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ describe( 'AxiosClient', () => {
|
|||
[ new AxiosResponseInterceptor() ],
|
||||
);
|
||||
|
||||
moxios.stubOnce( 'GET', '/test', {
|
||||
moxios.stubRequest( '/test', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
@ -19,7 +19,7 @@ describe( 'AxiosResponseInterceptor', () => {
|
|||
} );
|
||||
|
||||
it( 'should transform responses into an HTTPResponse', async () => {
|
||||
moxios.stubOnce( 'GET', 'http://test.test', {
|
||||
moxios.stubRequest( 'http://test.test', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -30,7 +30,7 @@ describe( 'AxiosResponseInterceptor', () => {
|
|||
const response = await axiosInstance.get( 'http://test.test' );
|
||||
|
||||
expect( response ).toMatchObject( {
|
||||
status: 200,
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
|
@ -41,7 +41,7 @@ describe( 'AxiosResponseInterceptor', () => {
|
|||
} );
|
||||
|
||||
it( 'should transform error responses into an HTTPResponse', async () => {
|
||||
moxios.stubOnce( 'GET', 'http://test.test', {
|
||||
moxios.stubRequest( 'http://test.test', {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -52,7 +52,7 @@ describe( 'AxiosResponseInterceptor', () => {
|
|||
const response = await axiosInstance.get( 'http://test.test' );
|
||||
|
||||
expect( response ).toMatchObject( {
|
||||
status: 404,
|
||||
statusCode: 404,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
|
|
|
@ -9,6 +9,12 @@ export class AxiosClient implements HTTPClient {
|
|||
private readonly client: AxiosInstance;
|
||||
private readonly interceptors: AxiosInterceptor[];
|
||||
|
||||
/**
|
||||
* Creates a new axios client.
|
||||
*
|
||||
* @param {AxiosRequestConfig} config The request configuration.
|
||||
* @param {AxiosInterceptor[]} interceptors An array of interceptors to apply to the client.
|
||||
*/
|
||||
public constructor( config: AxiosRequestConfig, interceptors: AxiosInterceptor[] = [] ) {
|
||||
this.client = axios.create( config );
|
||||
this.interceptors = interceptors;
|
||||
|
|
|
@ -9,6 +9,12 @@ import { AxiosInterceptor } from './axios-interceptor';
|
|||
export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||
private oauth: OAuth;
|
||||
|
||||
/**
|
||||
* Creates a new interceptor.
|
||||
*
|
||||
* @param {string} consumerKey The consumer key of the API key.
|
||||
* @param {string} consumerSecret The consumer secret of the API key.
|
||||
*/
|
||||
public constructor( consumerKey: string, consumerSecret: string ) {
|
||||
super();
|
||||
|
||||
|
|
|
@ -2,12 +2,19 @@
|
|||
* A structured response from the HTTP client.
|
||||
*/
|
||||
export class HTTPResponse< T = any > {
|
||||
public readonly status: number;
|
||||
public readonly statusCode: number;
|
||||
public readonly headers: any;
|
||||
public readonly data: T;
|
||||
|
||||
public constructor( status: number, headers: any, data: T ) {
|
||||
this.status = status;
|
||||
/**
|
||||
* Creates a new HTTP response instance.
|
||||
*
|
||||
* @param {number} statusCode The status code from the HTTP response.
|
||||
* @param {*} headers The headers from the HTTP response.
|
||||
* @param {*} data The data from the HTTP response.
|
||||
*/
|
||||
public constructor( statusCode: number, headers: any, data: T ) {
|
||||
this.statusCode = statusCode;
|
||||
this.headers = headers;
|
||||
this.data = data;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue