Revised the ModelTransformer so that it can support more complicated transformations.
This commit is contained in:
parent
67f57abe26
commit
46df060c0e
|
@ -21,7 +21,8 @@
|
|||
"!*.tsbuildinfo",
|
||||
"!/dist/**/__tests__/",
|
||||
"!/dist/**/__mocks__/",
|
||||
"!/dist/**/__snapshops__/"
|
||||
"!/dist/**/__snapshops__/",
|
||||
"!/dist/**/__test_data__/"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Model } from '../models/model';
|
||||
|
||||
/**
|
||||
* A dummy model that can be used in test files.
|
||||
*/
|
||||
export class DummyModel extends Model {
|
||||
public name: string = '';
|
||||
|
||||
public constructor( partial?: Partial< DummyModel > ) {
|
||||
super();
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
}
|
|
@ -12,15 +12,8 @@ import {
|
|||
UpdatesChildModels,
|
||||
UpdatesModels,
|
||||
} from '../model-repository';
|
||||
import { DummyModel } from '../../__test_data__/dummy-model';
|
||||
|
||||
class DummyModel extends Model {
|
||||
public name: string = '';
|
||||
|
||||
public constructor( partial?: Partial< DummyModel > ) {
|
||||
super();
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
}
|
||||
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
|
||||
|
||||
class DummyChildModel extends Model {
|
||||
|
|
|
@ -1,90 +1,90 @@
|
|||
import { Model } from '../../models/model';
|
||||
import { ModelTransformer } from '../model-transformer';
|
||||
import { ModelTransformation, ModelTransformer } from '../model-transformer';
|
||||
import { DummyModel } from '../../__test_data__/dummy-model';
|
||||
|
||||
class DummyModel extends Model {
|
||||
public name: string = '';
|
||||
class DummyTransformation implements ModelTransformation {
|
||||
public readonly priority: number;
|
||||
|
||||
public constructor( partial?: Partial< DummyModel > ) {
|
||||
super();
|
||||
Object.assign( this, partial );
|
||||
private readonly fn: ( ( p: any ) => any ) | null;
|
||||
|
||||
public constructor( priority: number, fn: ( ( p: any ) => any ) | null ) {
|
||||
this.priority = priority;
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
public fromModel( properties: any ): any {
|
||||
if ( ! this.fn ) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
return this.fn( properties );
|
||||
}
|
||||
|
||||
public toModel( properties: any ): any {
|
||||
if ( ! this.fn ) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
return this.fn( properties );
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'ModelTransformer', () => {
|
||||
describe( 'fromModel', () => {
|
||||
it( 'should convert models plainly', () => {
|
||||
const model = new DummyModel( { name: 'Test' } );
|
||||
it( 'should prioritize transformers correctly', () => {
|
||||
const fn1 = jest.fn();
|
||||
fn1.mockReturnValue( { name: 'fn1' } );
|
||||
const fn2 = jest.fn();
|
||||
fn2.mockReturnValue( { name: 'fn2' } );
|
||||
|
||||
const transformed = ModelTransformer.fromModel( model );
|
||||
const transformer = new ModelTransformer< DummyModel >(
|
||||
[
|
||||
// Ensure the priorities are backwards so sorting is tested.
|
||||
new DummyTransformation( 1, fn2 ),
|
||||
new DummyTransformation( 0, fn1 ),
|
||||
],
|
||||
);
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
name: 'Test',
|
||||
},
|
||||
);
|
||||
} );
|
||||
const transformed = transformer.toModel( DummyModel, { name: 'fn0' } );
|
||||
|
||||
it( 'should convert models with key changes', () => {
|
||||
const model = new DummyModel( { name: 'Test' } );
|
||||
|
||||
const transformed = ModelTransformer.fromModel(
|
||||
model,
|
||||
{ name: 'new-test' },
|
||||
);
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
'new-test': 'Test',
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should convert models with transformations', () => {
|
||||
const model = new DummyModel( { name: 'Test' } );
|
||||
|
||||
const transformed = ModelTransformer.fromModel(
|
||||
model,
|
||||
undefined,
|
||||
{ name: ( val ) => 'Transform-' + val },
|
||||
);
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
name: 'Transform-Test',
|
||||
},
|
||||
);
|
||||
} );
|
||||
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn0' } );
|
||||
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn1' } );
|
||||
expect( transformed ).toMatchObject( { name: 'fn2' } );
|
||||
} );
|
||||
|
||||
describe( 'toModel', () => {
|
||||
it( 'should create models plainly', () => {
|
||||
const transformed = ModelTransformer.toModel(
|
||||
DummyModel,
|
||||
{ name: 'Test' },
|
||||
);
|
||||
it( 'should transform to model', () => {
|
||||
const transformer = new ModelTransformer< DummyModel >(
|
||||
[
|
||||
new DummyTransformation(
|
||||
0,
|
||||
( p: any ) => {
|
||||
p.name = 'Transformed-' + p.name;
|
||||
return p;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect( transformed ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
} );
|
||||
const model = transformer.toModel( DummyModel, { name: 'Test' } );
|
||||
|
||||
it( 'should create models with key changes', () => {
|
||||
const transformed = ModelTransformer.toModel(
|
||||
DummyModel,
|
||||
{ test: 'Test' },
|
||||
{ test: 'name' },
|
||||
);
|
||||
expect( model ).toBeInstanceOf( DummyModel );
|
||||
expect( model.name ).toEqual( 'Transformed-Test' );
|
||||
} );
|
||||
|
||||
expect( transformed ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
} );
|
||||
it( 'should transform from model', () => {
|
||||
const transformer = new ModelTransformer< DummyModel >(
|
||||
[
|
||||
new DummyTransformation(
|
||||
0,
|
||||
( p: any ) => {
|
||||
p.name = 'Transformed-' + p.name;
|
||||
return p;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
it( 'should create models with transformations', () => {
|
||||
const transformed = ModelTransformer.toModel(
|
||||
DummyModel,
|
||||
{ name: 'Test' },
|
||||
undefined,
|
||||
{ name: ( val ) => 'Transform-' + val },
|
||||
);
|
||||
const transformed = transformer.fromModel( new DummyModel( { name: 'Test' } ) );
|
||||
|
||||
expect( transformed ).toMatchObject( new DummyModel( { name: 'Transform-Test' } ) );
|
||||
} );
|
||||
expect( transformed ).not.toBeInstanceOf( DummyModel );
|
||||
expect( transformed.name ).toEqual( 'Transformed-Test' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1,92 +1,76 @@
|
|||
import { Model } from '../models/model';
|
||||
|
||||
/**
|
||||
* A map for converting between API/Model keys.
|
||||
* An interface for an object that can perform transformations both to and from a representation
|
||||
* and return the input data after performing the desired transformation.
|
||||
*
|
||||
* @typedef KeyChangeMap
|
||||
* @alias Object.<string,string>
|
||||
* @interface ModelTransformation
|
||||
*/
|
||||
type KeyChangeMap< F, T > = {
|
||||
[ key in keyof F ]?: keyof T;
|
||||
export interface ModelTransformation {
|
||||
readonly priority: number;
|
||||
toModel( properties: any ): any;
|
||||
fromModel( properties: any ): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map for transformations between API/Model data.
|
||||
*
|
||||
* @typedef TransformationMap
|
||||
* @alias Object.<string,Function>
|
||||
* A class for transforming models to/from a generic representation.
|
||||
*/
|
||||
type TransformationMap< T > = {
|
||||
[ key in keyof T ]?: ( val: any ) => any;
|
||||
}
|
||||
|
||||
export namespace ModelTransformer {
|
||||
export class ModelTransformer< T extends Model > {
|
||||
/**
|
||||
* Given an object, this function will transform it using the described key changes and transformations.
|
||||
* An array of transformations to use when converting data to/from models.
|
||||
*
|
||||
* @param {*} data
|
||||
* @param {KeyChangeMap} keyChanges A map of keys to change into another key.
|
||||
* @param {TransformationMap} transformations A map of transformations to perform on values keyed by the key.
|
||||
* @return {*} The transformed data.
|
||||
* @type {Array.<ModelTransformation>}
|
||||
* @private
|
||||
*/
|
||||
function transform< F, T >(
|
||||
data: F,
|
||||
keyChanges?: KeyChangeMap< F, T >,
|
||||
transformations?: TransformationMap< F >,
|
||||
): any {
|
||||
const transformed: any = {};
|
||||
|
||||
for ( const inputKey in data ) {
|
||||
let key: string = inputKey;
|
||||
if ( keyChanges && keyChanges.hasOwnProperty( inputKey ) ) {
|
||||
key = keyChanges[ inputKey ] as string;
|
||||
}
|
||||
|
||||
let value = data[ inputKey ];
|
||||
if ( transformations && transformations.hasOwnProperty( inputKey ) ) {
|
||||
value = ( transformations[ inputKey ] as ( val: any ) => any )( value );
|
||||
}
|
||||
|
||||
transformed[ key ] = value;
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
private transformations: readonly ModelTransformation[];
|
||||
|
||||
/**
|
||||
* Given a model this will transform it into a standard object.
|
||||
* Creates a new model transformer instance.
|
||||
*
|
||||
* @param {F} data The model data to transform.
|
||||
* @param {KeyChangeMap} keyChanges A map of keys to change into another key.
|
||||
* @param {TransformationMap} transformations A map of transformations to perform on values keyed by the key.
|
||||
* @return {*} The transformed data.
|
||||
* @template F
|
||||
* @param {Array.<ModelTransformation>} transformations The transformations to use.
|
||||
*/
|
||||
export function fromModel< F extends Partial< Model > >(
|
||||
data: F,
|
||||
keyChanges?: KeyChangeMap< F, any >,
|
||||
transformations?: TransformationMap< F >,
|
||||
): any {
|
||||
return transform( data, keyChanges, transformations );
|
||||
public constructor( transformations: ModelTransformation[] ) {
|
||||
// Ensure that the transformations are sorted by priority.
|
||||
transformations.sort( ( a, b ) => ( a.priority > b.priority ) ? 1 : -1 );
|
||||
|
||||
this.transformations = transformations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object this will transform it into a model object.
|
||||
* Takes the input data and runs all of the transformations on it before returning the created model.
|
||||
*
|
||||
* @param {Function} modelClass The model class we want to instantiate.
|
||||
* @param {*} data The raw data to transform.
|
||||
* @param {KeyChangeMap} keyChanges A map of keys to change into another key.
|
||||
* @param {TransformationMap} transformations A map of transformations to perform on values keyed by the key.
|
||||
* @return {T} The created model.
|
||||
* @param {Function.<T>} modelClass The model class we're trying to create.
|
||||
* @param {*} data The data we're transforming.
|
||||
* @return {T} The transformed model.
|
||||
* @template T
|
||||
*/
|
||||
export function toModel< T extends Model, F >(
|
||||
modelClass: new ( properties: Partial< T > ) => T,
|
||||
data: F,
|
||||
keyChanges?: KeyChangeMap< F, T >,
|
||||
transformations?: TransformationMap< F >,
|
||||
): T {
|
||||
return new modelClass( transform( data, keyChanges, transformations ) );
|
||||
public toModel( modelClass: new( properties: Partial< T > ) => T, data: any ): T {
|
||||
const transformed: any = this.transformations.reduce(
|
||||
( properties: any, transformer: ModelTransformation ) => {
|
||||
return transformer.toModel( properties );
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
return new modelClass( transformed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the input model and runs all of the transformations on it before returning the data.
|
||||
*
|
||||
* @param {Partial.<T>} model The model to transform.
|
||||
* @return {*} The transformed data.
|
||||
* @template T
|
||||
*/
|
||||
public fromModel( model: Partial< T > ): any {
|
||||
// Convert the model class to raw properties so that the transformations can be simple.
|
||||
const raw = JSON.parse( JSON.stringify( model ) );
|
||||
|
||||
return this.transformations.reduce(
|
||||
( properties: any, transformer: ModelTransformation ) => {
|
||||
return transformer.fromModel( properties );
|
||||
},
|
||||
raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { KeyChangeTransformation } from '../key-change-transformation';
|
||||
import { DummyModel } from '../../../__test_data__/dummy-model';
|
||||
|
||||
describe( 'KeyChangeTransformation', () => {
|
||||
let transformation: KeyChangeTransformation< DummyModel >;
|
||||
|
||||
beforeEach( () => {
|
||||
transformation = new KeyChangeTransformation<DummyModel>(
|
||||
{
|
||||
name: 'new-name',
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should transform to model', () => {
|
||||
const transformed = transformation.toModel( { 'new-name': 'Test Name' } );
|
||||
|
||||
expect( transformed ).toHaveProperty( 'name', 'Test Name' );
|
||||
expect( transformed ).not.toHaveProperty( 'new-name' );
|
||||
} );
|
||||
|
||||
it( 'should transform from model', () => {
|
||||
const transformed = transformation.fromModel( { name: 'Test Name' } );
|
||||
|
||||
expect( transformed ).toHaveProperty( 'new-name', 'Test Name' );
|
||||
expect( transformed ).not.toHaveProperty( 'name' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,74 @@
|
|||
import { ModelTransformation } from '../model-transformer';
|
||||
import { Model } from '../../models/model';
|
||||
|
||||
/**
|
||||
* @typedef KeyChanges
|
||||
* @alias Object.<string,string>
|
||||
*/
|
||||
type KeyChanges< T extends Model > = { readonly [ key in keyof Partial< T > ]: string };
|
||||
|
||||
/**
|
||||
* A model transformation that can be used to change property keys between two formats.
|
||||
* This transformation has a very high priority so that it will be executed after all
|
||||
* other transformations to prevent the changed key from causing problems.
|
||||
*/
|
||||
export class KeyChangeTransformation< T extends Model > implements ModelTransformation {
|
||||
/**
|
||||
* Ensure that this transformation always happens at the very end since it changes the keys
|
||||
* in the transformed object.
|
||||
*/
|
||||
public readonly priority = 999999;
|
||||
|
||||
/**
|
||||
* The key change transformations that this object should perform.
|
||||
* This is structured with the model's property key as the key
|
||||
* of the object and the raw property key as the value.
|
||||
*
|
||||
* @type {KeyChanges}
|
||||
* @private
|
||||
*/
|
||||
private readonly changes: KeyChanges< T >;
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {KeyChanges} changes The changes we want the transformation to make.
|
||||
*/
|
||||
public constructor( changes: KeyChanges< T > ) {
|
||||
this.changes = changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
for ( const key in this.changes ) {
|
||||
const value = this.changes[ key ];
|
||||
|
||||
properties[ value ] = properties[ key ];
|
||||
delete properties[ key ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
for ( const key in this.changes ) {
|
||||
const value = this.changes[ key ];
|
||||
|
||||
properties[ key ] = properties[ value ];
|
||||
delete properties[ value ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -3,30 +3,32 @@ import { CreateFn, ModelRepository } from '../../../framework/model-repository';
|
|||
import { SimpleProduct } from '../../../models';
|
||||
import { CreatesSimpleProducts, SimpleProductRepositoryParams } from '../../../models/products/simple-product';
|
||||
import { ModelTransformer } from '../../../framework/model-transformer';
|
||||
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
|
||||
|
||||
function fromServer( data: any ): SimpleProduct {
|
||||
if ( ! data.id ) {
|
||||
throw new Error( 'An invalid response was received.' );
|
||||
}
|
||||
|
||||
return ModelTransformer.toModel(
|
||||
SimpleProduct,
|
||||
data,
|
||||
{
|
||||
regular_price: 'regularPrice',
|
||||
},
|
||||
const t = new ModelTransformer< SimpleProduct >(
|
||||
[
|
||||
new KeyChangeTransformation< SimpleProduct >( { regularPrice: 'regular_price' } ),
|
||||
],
|
||||
);
|
||||
|
||||
return t.toModel( SimpleProduct, data );
|
||||
}
|
||||
|
||||
function toServer( model: Partial< SimpleProduct > ): any {
|
||||
const t = new ModelTransformer< SimpleProduct >(
|
||||
[
|
||||
new KeyChangeTransformation< SimpleProduct >( { regularPrice: 'regular_price' } ),
|
||||
],
|
||||
);
|
||||
|
||||
return Object.assign(
|
||||
{ type: 'simple' },
|
||||
ModelTransformer.fromModel(
|
||||
model,
|
||||
{
|
||||
regularPrice: 'regular_price',
|
||||
},
|
||||
),
|
||||
t.fromModel( model ),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,19 +3,20 @@ import { ListFn, ModelRepository } from '../../../framework/model-repository';
|
|||
import { SettingGroup } from '../../../models';
|
||||
import { ListsSettingGroups, SettingGroupRepositoryParams } from '../../../models/settings/setting-group';
|
||||
import { ModelTransformer } from '../../../framework/model-transformer';
|
||||
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
|
||||
|
||||
function fromServer( data: any ): SettingGroup {
|
||||
if ( ! data.id ) {
|
||||
throw new Error( 'An invalid response was received.' );
|
||||
}
|
||||
|
||||
return ModelTransformer.toModel(
|
||||
SettingGroup,
|
||||
data,
|
||||
{
|
||||
parent_id: 'parentID',
|
||||
},
|
||||
const t = new ModelTransformer< SettingGroup >(
|
||||
[
|
||||
new KeyChangeTransformation< SettingGroup >( { parentID: 'parent_id' } ),
|
||||
],
|
||||
);
|
||||
|
||||
return t.toModel( SettingGroup, data );
|
||||
}
|
||||
|
||||
function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParams > {
|
||||
|
|
|
@ -19,7 +19,8 @@ function fromServer( data: any ): Setting {
|
|||
throw new Error( 'An invalid response was received.' );
|
||||
}
|
||||
|
||||
return ModelTransformer.toModel( Setting, data );
|
||||
const t = new ModelTransformer< Setting >( [] );
|
||||
return t.toModel( Setting, data );
|
||||
}
|
||||
|
||||
function restList( httpClient: HTTPClient ): ListChildFn< SettingRepositoryParams > {
|
||||
|
|
Loading…
Reference in New Issue