Refactored the factory to use a repository instead of an adapter so that we can expose more API functionality

This commit is contained in:
Christopher Allford 2020-09-07 15:38:12 -07:00
parent a875ecb083
commit 39c5bc6b74
24 changed files with 301 additions and 541 deletions

View File

@ -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',

View File

@ -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[]>;
}

View File

@ -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' );
} );
} );

View File

@ -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 );
}
}

View File

@ -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 >>;
}

View File

@ -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/ );
} );
} );

View File

@ -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() ],
);
}
}

View File

@ -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';

View File

@ -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' );
} );
} );

View File

@ -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 );
}
}

View File

@ -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;
}
}

View File

@ -1,4 +1,4 @@
import { Model } from '../framework/model';
import { Model } from './model';
import { DeepPartial } from 'fishery';
/**

View File

@ -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 );
}

View File

@ -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",

View File

@ -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"

View File

@ -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' );
} );
} );

View File

@ -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' } );
} );
} );

View File

@ -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 );
}
}

View File

@ -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 >;
}

View File

@ -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 ];
}
}

View File

@ -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 {
/**

View File

@ -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';

View File

@ -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;
}
}

View File

@ -1,6 +1,6 @@
{
"references": [
{ "path": "tests/e2e/factories" }
{ "path": "tests/e2e/api" }
],
"files": []
}