Merge pull request #28206 from woocommerce/packages/api/add/simple-product

@woocommerce/api: Complete SimpleProduct implementation
This commit is contained in:
Christopher Allford 2020-11-10 15:11:52 -08:00 committed by GitHub
commit 28f5704ff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2557 additions and 268 deletions

View File

@ -21,7 +21,8 @@
"!*.tsbuildinfo", "!*.tsbuildinfo",
"!/dist/**/__tests__/", "!/dist/**/__tests__/",
"!/dist/**/__mocks__/", "!/dist/**/__mocks__/",
"!/dist/**/__snapshops__/" "!/dist/**/__snapshops__/",
"!/dist/**/__test_data__/"
], ],
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {

View File

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

View File

@ -12,15 +12,8 @@ import {
UpdatesChildModels, UpdatesChildModels,
UpdatesModels, UpdatesModels,
} from '../model-repository'; } 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' > type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
class DummyChildModel extends Model { class DummyChildModel extends Model {

View File

@ -0,0 +1,100 @@
import { ModelTransformation, ModelTransformer } from '../model-transformer';
import { DummyModel } from '../../__test_data__/dummy-model';
class DummyTransformation implements ModelTransformation {
public readonly fromModelOrder: number;
private readonly fn: ( ( p: any ) => any ) | null;
public constructor( order: number, fn: ( ( p: any ) => any ) | null ) {
this.fromModelOrder = order;
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', () => {
it( 'should order transformers correctly', () => {
const fn1 = jest.fn();
fn1.mockReturnValue( { name: 'fn1' } );
const fn2 = jest.fn();
fn2.mockReturnValue( { name: 'fn2' } );
const transformer = new ModelTransformer< DummyModel >(
[
// Ensure the orders are backwards so sorting is tested.
new DummyTransformation( 1, fn2 ),
new DummyTransformation( 0, fn1 ),
],
);
let transformed = transformer.fromModel( new DummyModel( { name: 'fn0' } ) );
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn0' } );
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn1' } );
expect( transformed ).toMatchObject( { name: 'fn2' } );
// Reset and make sure "toModel" happens in reverse order.
fn1.mockClear();
fn2.mockClear();
transformed = transformer.toModel( DummyModel, { name: 'fn3' } );
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn3' } );
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn2' } );
expect( transformed ).toMatchObject( { name: 'fn1' } );
} );
it( 'should transform to model', () => {
const transformer = new ModelTransformer< DummyModel >(
[
new DummyTransformation(
0,
( p: any ) => {
p.name = 'Transformed-' + p.name;
return p;
},
),
],
);
const model = transformer.toModel( DummyModel, { name: 'Test' } );
expect( model ).toBeInstanceOf( DummyModel );
expect( model.name ).toEqual( 'Transformed-Test' );
} );
it( 'should transform from model', () => {
const transformer = new ModelTransformer< DummyModel >(
[
new DummyTransformation(
0,
( p: any ) => {
p.name = 'Transformed-' + p.name;
return p;
},
),
],
);
const transformed = transformer.fromModel( new DummyModel( { name: 'Test' } ) );
expect( transformed ).not.toBeInstanceOf( DummyModel );
expect( transformed.name ).toEqual( 'Transformed-Test' );
} );
} );

View File

@ -31,13 +31,14 @@ export interface ModelRepositoryParams<
/** /**
* These helpers will extract information about a model from its repository params to be used in the repository. * These helpers will extract information about a model from its repository params to be used in the repository.
*/ */
type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never; export type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never;
type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never; export type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never;
export type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
type ListParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, any, infer X > ] ? X : never; type ListParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, any, infer X > ] ? X : never;
type PickUpdateParams<T, K extends keyof T> = { [P in K]?: T[P]; };
type UpdateParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer C, any, any, infer X > ] ? type UpdateParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer C, any, any, infer X > ] ?
( [ X ] extends [ keyof C ] ? Pick< C, X > : never ) : ( [ X ] extends [ keyof C ] ? PickUpdateParams< C, X > : never ) :
never; never;
type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
/** /**
* A callback for listing models using a data source. * A callback for listing models using a data source.

View File

@ -0,0 +1,113 @@
import { Model } from '../models/model';
import { ModelConstructor } from '../models/shared-types';
/**
* 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.
*
* @interface ModelTransformation
*/
export interface ModelTransformation {
/**
* The order of execution for the transformer.
* - For "fromModel" higher numbers execute later.
* - For "toModel" the order is reversed.
*
* @type {number}
*/
readonly fromModelOrder: number;
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
fromModel( properties: any ): any;
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
toModel( properties: any ): any;
}
/**
* An enum for defining the "toModel" transformation order values.
*/
export enum TransformationOrder {
First = 0,
Normal = 500000,
Last = 1000000,
/**
* A special value reserved for transformations that MUST come after all orders due to
* the way that they destroy the property keys or values.
*/
VeryLast = 2000000
}
/**
* A class for transforming models to/from a generic representation.
*/
export class ModelTransformer< T extends Model > {
/**
* An array of transformations to use when converting data to/from models.
*
* @type {Array.<ModelTransformation>}
* @private
*/
private transformations: readonly ModelTransformation[];
/**
* Creates a new model transformer instance.
*
* @param {Array.<ModelTransformation>} transformations The transformations to use.
*/
public constructor( transformations: ModelTransformation[] ) {
// Ensure that the transformations are sorted by priority.
transformations.sort( ( a, b ) => ( a.fromModelOrder > b.fromModelOrder ) ? 1 : -1 );
this.transformations = transformations;
}
/**
* 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 = Object.assign( {}, model );
return this.transformations.reduce(
( properties: any, transformer: ModelTransformation ) => {
return transformer.fromModel( properties );
},
raw,
);
}
/**
* Takes the input data and runs all of the transformations on it before returning 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
*/
public toModel( modelClass: ModelConstructor< T >, data: any ): T {
const transformed: any = this.transformations.reduceRight(
( properties: any, transformer: ModelTransformation ) => {
return transformer.toModel( properties );
},
data,
);
return new modelClass( transformed );
}
}

View File

@ -0,0 +1,56 @@
import { AddPropertyTransformation } from '../add-property-transformation';
describe( 'AddPropertyTransformation', () => {
let transformation: AddPropertyTransformation;
beforeEach( () => {
transformation = new AddPropertyTransformation(
{ toProperty: 'Test' },
{ fromProperty: 'Test' },
);
} );
it( 'should add property when missing', () => {
let transformed = transformation.toModel( { id: 1, name: 'Test' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
toProperty: 'Test',
},
);
transformed = transformation.fromModel( { id: 1, name: 'Test' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
fromProperty: 'Test',
},
);
} );
it( 'should not add property when present', () => {
let transformed = transformation.toModel( { id: 1, name: 'Test', toProperty: 'Existing' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
toProperty: 'Existing',
},
);
transformed = transformation.fromModel( { id: 1, name: 'Test', fromProperty: 'Existing' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
fromProperty: 'Existing',
},
);
} );
} );

View File

@ -0,0 +1,26 @@
import { CustomTransformation } from '../custom-transformation';
describe( 'CustomTransformation', () => {
it( 'should do nothing without hooks', () => {
const transformation = new CustomTransformation( 0, null, null );
const expected = { test: 'Test' };
expect( transformation.toModel( expected ) ).toMatchObject( expected );
expect( transformation.fromModel( expected ) ).toMatchObject( expected );
} );
it( 'should execute hooks', () => {
const toHook = jest.fn();
toHook.mockReturnValue( { toModel: 'Test' } );
const fromHook = jest.fn();
fromHook.mockReturnValue( { fromModel: 'Test' } );
const transformation = new CustomTransformation( 0, toHook, fromHook );
expect( transformation.toModel( { test: 'Test' } ) ).toMatchObject( { toModel: 'Test' } );
expect( toHook ).toHaveBeenCalledWith( { test: 'Test' } );
expect( transformation.fromModel( { test: 'Test' } ) ).toMatchObject( { fromModel: 'Test' } );
expect( fromHook ).toHaveBeenCalledWith( { test: 'Test' } );
} );
} );

View File

@ -0,0 +1,31 @@
import { IgnorePropertyTransformation } from '../ignore-property-transformation';
describe( 'IgnorePropertyTransformation', () => {
let transformation: IgnorePropertyTransformation;
beforeEach( () => {
transformation = new IgnorePropertyTransformation( [ 'skip' ] );
} );
it( 'should remove ignored properties', () => {
let transformed = transformation.fromModel(
{
test: 'Test',
skip: 'Test',
},
);
expect( transformed ).toHaveProperty( 'test', 'Test' );
expect( transformed ).not.toHaveProperty( 'skip' );
transformed = transformation.toModel(
{
test: 'Test',
skip: 'Test',
},
);
expect( transformed ).toHaveProperty( 'test', 'Test' );
expect( transformed ).not.toHaveProperty( 'skip' );
} );
} );

View File

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

View File

@ -0,0 +1,52 @@
import { ModelTransformerTransformation } from '../model-transformer-transformation';
import { ModelTransformer } from '../../model-transformer';
import { mock, MockProxy } from 'jest-mock-extended';
import { DummyModel } from '../../../__test_data__/dummy-model';
describe( 'ModelTransformerTransformation', () => {
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
let transformation: ModelTransformerTransformation< any >;
beforeEach( () => {
mockTransformer = mock< ModelTransformer< any > >();
transformation = new ModelTransformerTransformation< DummyModel >(
'test',
DummyModel,
mockTransformer,
);
} );
it( 'should execute child transformer', () => {
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
let transformed = transformation.toModel( { test: 'Test' } );
expect( transformed ).toMatchObject( { test: { toModel: 'Test' } } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
transformed = transformation.fromModel( { test: 'Test' } );
expect( transformed ).toMatchObject( { test: { fromModel: 'Test' } } );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
} );
it( 'should execute child transformer on array', () => {
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
let transformed = transformation.toModel( { test: [ 'Test', 'Test2' ] } );
expect( transformed ).toMatchObject( { test: [ { toModel: 'Test' }, { toModel: 'Test' } ] } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test2' );
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
transformed = transformation.fromModel( { test: [ 'Test', 'Test2' ] } );
expect( transformed ).toMatchObject( { test: [ { fromModel: 'Test' }, { fromModel: 'Test' } ] } );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test2' );
} );
} );

View File

@ -0,0 +1,108 @@
import { PropertyType, PropertyTypeTransformation } from '../property-type-transformation';
describe( 'PropertyTypeTransformation', () => {
let transformation: PropertyTypeTransformation;
beforeEach( () => {
transformation = new PropertyTypeTransformation(
{
string: PropertyType.String,
integer: PropertyType.Integer,
float: PropertyType.Float,
boolean: PropertyType.Boolean,
date: PropertyType.Date,
callback: ( value: string ) => 'Transformed-' + value,
},
);
} );
it( 'should convert strings', () => {
let transformed = transformation.toModel( { string: 'Test' } );
expect( transformed.string ).toStrictEqual( 'Test' );
transformed = transformation.fromModel( { string: 'Test' } );
expect( transformed.string ).toStrictEqual( 'Test' );
} );
it( 'should convert integers', () => {
let transformed = transformation.toModel( { integer: '100' } );
expect( transformed.integer ).toStrictEqual( 100 );
transformed = transformation.fromModel( { integer: 100 } );
expect( transformed.integer ).toStrictEqual( '100' );
} );
it( 'should convert floats', () => {
let transformed = transformation.toModel( { float: '2.5' } );
expect( transformed.float ).toStrictEqual( 2.5 );
transformed = transformation.fromModel( { float: 2.5 } );
expect( transformed.float ).toStrictEqual( '2.5' );
} );
it( 'should convert booleans', () => {
let transformed = transformation.toModel( { boolean: 'true' } );
expect( transformed.boolean ).toStrictEqual( true );
transformed = transformation.fromModel( { boolean: false } );
expect( transformed.boolean ).toStrictEqual( 'false' );
} );
it( 'should convert dates', () => {
let transformed = transformation.toModel( { date: '2020-11-06T03:11:41.000Z' } );
expect( transformed.date ).toStrictEqual( new Date( '2020-11-06T03:11:41.000Z' ) );
transformed = transformation.fromModel( { date: new Date( '2020-11-06T03:11:41.000Z' ) } );
expect( transformed.date ).toStrictEqual( '2020-11-06T03:11:41.000Z' );
} );
it( 'should use conversion callbacks', () => {
let transformed = transformation.toModel( { callback: 'Test' } );
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
transformed = transformation.fromModel( { callback: 'Test' } );
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
} );
it( 'should convert arrays', () => {
let transformed = transformation.toModel( { integer: [ '100', '200', '300' ] } );
expect( transformed.integer ).toStrictEqual( [ 100, 200, 300 ] );
transformed = transformation.fromModel( { integer: [ 100, 200, 300 ] } );
expect( transformed.integer ).toStrictEqual( [ '100', '200', '300' ] );
} );
it( 'should do nothing without property', () => {
let transformed = transformation.toModel( { name: 'Test' } );
expect( transformed.name ).toStrictEqual( 'Test' );
transformed = transformation.fromModel( { name: 'Test' } );
expect( transformed.name ).toStrictEqual( 'Test' );
} );
it( 'should preserve null', () => {
let transformed = transformation.toModel( { integer: null } );
expect( transformed.integer ).toStrictEqual( null );
transformed = transformation.fromModel( { integer: null } );
expect( transformed.integer ).toStrictEqual( null );
} );
} );

View File

@ -0,0 +1,76 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
/**
* @typedef AdditionalProperties
* @alias Object.<string,string>
*/
type AdditionalProperties = { [ key: string ]: any };
/**
* A model transformation that adds a property with
* a default value if it is not already set.
*/
export class AddPropertyTransformation implements ModelTransformation {
public readonly fromModelOrder = TransformationOrder.Normal;
/**
*The additional properties to add when executing toModel.
*
* @type {AdditionalProperties}
* @private
*/
private readonly toProperties: AdditionalProperties;
/**
* The additional properties to add when executing fromModel.
*
* @type {AdditionalProperties}
* @private
*/
private readonly fromProperties: AdditionalProperties;
/**
* Creates a new transformation.
*
* @param {AdditionalProperties} toProperties The properties to add when executing toModel.
* @param {AdditionalProperties} fromProperties The properties to add when executing fromModel.
*/
public constructor( toProperties: AdditionalProperties, fromProperties: AdditionalProperties ) {
this.toProperties = toProperties;
this.fromProperties = fromProperties;
}
/**
* 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.fromProperties ) {
if ( properties.hasOwnProperty( key ) ) {
continue;
}
properties[ key ] = this.fromProperties[ 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.toProperties ) {
if ( properties.hasOwnProperty( key ) ) {
continue;
}
properties[ key ] = this.toProperties[ key ];
}
return properties;
}
}

View File

@ -0,0 +1,78 @@
import { ModelTransformation } from '../model-transformer';
/**
* A callback for transforming model properties.
*
* @callback TransformationCallback
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
type TransformationCallback = ( properties: any ) => any;
/**
* A model transformer for executing arbitrary callbacks on input properties.
*/
export class CustomTransformation implements ModelTransformation {
public readonly fromModelOrder: number;
/**
* The hook to run for toModel.
*
* @type {TransformationCallback|null}
* @private
*/
private readonly toHook: TransformationCallback | null;
/**
* The hook to run for fromModel.
*
* @type {TransformationCallback|null}
* @private
*/
private readonly fromHook: TransformationCallback | null;
/**
* Creates a new transformation.
*
* @param {number} order The order for the transformation.
* @param {TransformationCallback|null} toHook The hook to run for toModel.
* @param {TransformationCallback|null} fromHook The hook to run for fromModel.
*/
public constructor(
order: number,
toHook: TransformationCallback | null,
fromHook: TransformationCallback | null,
) {
this.fromModelOrder = order;
this.toHook = toHook;
this.fromHook = fromHook;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
if ( ! this.fromHook ) {
return properties;
}
return this.fromHook( 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 {
if ( ! this.toHook ) {
return properties;
}
return this.toHook( properties );
}
}

View File

@ -0,0 +1,50 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
export class IgnorePropertyTransformation implements ModelTransformation {
public readonly fromModelOrder = TransformationOrder.Normal;
/**
* A list of properties that should be removed.
*
* @type {Array.<string>}
* @private
*/
private readonly ignoreList: readonly string[];
/**
* Creates a new transformation.
*
* @param {Array.<string>} ignoreList The properties to ignore.
*/
public constructor( ignoreList: string[] ) {
this.ignoreList = ignoreList;
}
/**
* 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 of this.ignoreList ) {
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 of this.ignoreList ) {
delete properties[ key ];
}
return properties;
}
}

View File

@ -0,0 +1,82 @@
import { ModelTransformation, TransformationOrder } 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 fromModelOrder = TransformationOrder.VeryLast + 1;
/**
* 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 ];
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
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 ];
if ( ! properties.hasOwnProperty( value ) ) {
continue;
}
properties[ key ] = properties[ value ];
delete properties[ value ];
}
return properties;
}
}

View File

@ -0,0 +1,89 @@
import { ModelTransformation, ModelTransformer, TransformationOrder } from '../model-transformer';
import { Model } from '../../models/model';
import { ModelConstructor } from '../../models/shared-types';
/**
* A model transformation that applies another transformer to a property.
*
* @template T
*/
export class ModelTransformerTransformation< T extends Model > implements ModelTransformation {
public readonly fromModelOrder = TransformationOrder.Normal;
/**
* The property that the transformation should be applied to.
*
* @type {string}
* @private
*/
private readonly property: string;
/**
* The model class we want to transform into.
*
* @type {Function.<T>}
* @private
* @template T
*/
private readonly modelClass: ModelConstructor< T >;
/**
* The transformer that should be used.
*
* @type {ModelTransformer}
* @private
*/
private readonly transformer: ModelTransformer< T >;
/**
* Creates a new transformation.
*
* @param {string} property The property we want to apply the transformer to.
* @param {ModelConstructor.<T>} modelClass The model to transform into.
* @param {ModelTransformer} transformer The transformer we want to apply.
* @template T
*/
public constructor( property: string, modelClass: ModelConstructor< T >, transformer: ModelTransformer< T > ) {
this.property = property;
this.modelClass = modelClass;
this.transformer = transformer;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
const val = properties[ this.property ];
if ( val ) {
if ( Array.isArray( val ) ) {
properties[ this.property ] = val.map( ( v ) => this.transformer.fromModel( v ) );
} else {
properties[ this.property ] = this.transformer.fromModel( val );
}
}
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 {
const val = properties[ this.property ];
if ( val ) {
if ( Array.isArray( val ) ) {
properties[ this.property ] = val.map( ( v ) => this.transformer.toModel( this.modelClass, v ) );
} else {
properties[ this.property ] = this.transformer.toModel( this.modelClass, val );
}
}
return properties;
}
}

View File

@ -0,0 +1,167 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
/**
* An enum defining all of the property types that we might want to transform.
*
* @enum {number}
*/
export enum PropertyType {
String,
Integer,
Float,
Boolean,
Date,
}
type PropertyTypeTypes = null | string | number | boolean | Date;
/**
* A callback that can be used to transform property types.
*
* @callback PropertyTypeCallback
* @param {*} value The value to transform.
* @return {*} The transformed value.
*/
type PropertyTypeCallback = ( value: any ) => any;
/**
* The types for all of a model's properties.
*
* @typedef PropertyTypes
* @alias Object.<string,PropertyType>
*/
type PropertyTypes = { [ key: string ]: PropertyType | PropertyTypeCallback };
/**
* A model transformer for converting property types between representation formats.
*/
export class PropertyTypeTransformation implements ModelTransformation {
/**
* We want the type transformation to take place after all of the others,
* since they may be operating on internal data types.
*/
public readonly fromModelOrder = TransformationOrder.VeryLast;
/**
* The property types we will want to transform.
*
* @type {PropertyTypes}
* @private
*/
private readonly types: PropertyTypes;
/**
* Creates a new transformation.
*
* @param {PropertyTypes} types The property types we want to transform.
*/
public constructor( types: PropertyTypes ) {
this.types = types;
}
/**
* 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.types ) {
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
const value = properties[ key ];
const type = this.types[ key ];
if ( type instanceof Function ) {
properties[ key ] = type( value );
continue;
}
properties[ key ] = this.convertFrom( value, type );
}
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.types ) {
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
const value = properties[ key ];
const type = this.types[ key ];
if ( type instanceof Function ) {
properties[ key ] = type( value );
continue;
}
properties[ key ] = this.convertTo( value, type );
}
return properties;
}
/**
* Converts the given value into the requested type.
*
* @param {*} value The value to transform.
* @param {PropertyType} type The type to transform it into.
* @return {*} The converted type.
* @private
*/
private convertTo( value: any, type: PropertyType ): PropertyTypeTypes | PropertyTypeTypes[] {
if ( Array.isArray( value ) ) {
return value.map( ( v: string ) => this.convertTo( v, type ) as PropertyTypeTypes );
}
if ( null === value ) {
return null;
}
switch ( type ) {
case PropertyType.String: return String( value );
case PropertyType.Integer: return parseInt( value );
case PropertyType.Float: return parseFloat( value );
case PropertyType.Boolean: return Boolean( value );
case PropertyType.Date:
return new Date( value );
}
}
/**
* Converts the given type into a string.
*
* @param {*} value The value to transform.
* @param {PropertyType} type The type to transform it into.
* @return {*} The converted type.
* @private
*/
private convertFrom( value: PropertyTypeTypes | PropertyTypeTypes[], type: PropertyType ): any {
if ( Array.isArray( value ) ) {
return value.map( ( v ) => this.convertFrom( v, type ) );
}
if ( null === value ) {
return null;
}
switch ( type ) {
case PropertyType.String:
case PropertyType.Integer:
case PropertyType.Float:
case PropertyType.Boolean:
return String( value );
case PropertyType.Date: {
return ( value as Date ).toISOString();
}
}
}
}

View File

@ -1,4 +1,32 @@
import { Model } from '../model'; import { Model } from '../model';
import { MetaData, PostStatus } from '../shared-types';
import {
BackorderStatus,
CatalogVisibility,
ProductAttribute,
ProductDownload,
ProductImage,
ProductTerm, StockStatus,
Taxability,
} from './shared-types';
/**
* The common parameters that all products can use in search.
*/
export type ProductSearchParams = { search: string };
/**
* The common parameters that all products can update.
*/
export type ProductUpdateParams = 'name' | 'slug' | 'created' | 'postStatus' | 'shortDescription'
| 'description' | 'sku' | 'categories' | 'tags' | 'isFeatured'
| 'isVirtual' | 'attributes' | 'images' | 'catalogVisibility'
| 'regularPrice' | 'onePerOrder' | 'taxStatus' | 'taxClass'
| 'salePrice' | 'saleStart' | 'saleEnd' | 'isDownloadable'
| 'downloadLimit' | 'daysToDownload' | 'weight' | 'length'
| 'width' | 'height' | 'trackInventory' | 'remainingStock'
| 'stockStatus' | 'backorderStatus' | 'allowReviews'
| 'metaData';
/** /**
* The base class for all product types. * The base class for all product types.
@ -11,10 +39,325 @@ export abstract class AbstractProduct extends Model {
*/ */
public readonly name: string = ''; public readonly name: string = '';
/**
* The slug of the product.
*
* @type {string}
*/
public readonly slug: string = '';
/**
* The permalink of the product.
*
* @type {string}
*/
public readonly permalink: string = '';
/**
* The GMT datetime when the product was created.
*
* @type {Date}
*/
public readonly created: Date = new Date();
/**
* The GMT datetime when the product was last modified.
*
* @type {Date}
*/
public readonly modified: Date = new Date();
/**
* The product's current post status.
*
* @type {PostStatus}
*/
public readonly postStatus: PostStatus = '';
/**
* The product's short description.
*
* @type {string}
*/
public readonly shortDescription: string = '';
/**
* The product's full description.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The product's SKU.
*
* @type {string}
*/
public readonly sku: string = '';
/**
* An array of the categories this product is in.
*
* @type {ReadonlyArray.<ProductTerm>}
*/
public readonly categories: readonly ProductTerm[] = [];
/**
* An array of the tags this product has.
*
* @type {ReadonlyArray.<ProductTerm>}
*/
public readonly tags: readonly ProductTerm[] = [];
/**
* Indicates whether or not the product is currently able to be purchased.
*
* @type {boolean}
*/
public readonly isPurchasable: boolean = true;
/**
* Indicates whether or not the product should be featured.
*
* @type {boolean}
*/
public readonly isFeatured: boolean = false;
/**
* Indicates that the product is delivered virtually.
*
* @type {boolean}
*/
public readonly isVirtual: boolean = false;
/**
* The attributes for the product.
*
* @type {ReadonlyArray.<ProductAttribute>}
*/
public readonly attributes: readonly ProductAttribute[] = [];
/**
* The images for the product.
*
* @type {ReadonlyArray.<ProductImage>}
*/
public readonly images: readonly ProductImage[] = [];
/**
* Indicates whether or not the product should be visible in the catalog.
*
* @type {CatalogVisibility}
*/
public readonly catalogVisibility: CatalogVisibility = CatalogVisibility.Everywhere;
/**
* The current price of the product.
*
* @type {string}
*/
public readonly price: string = '';
/** /**
* The regular price of the product when not discounted. * The regular price of the product when not discounted.
* *
* @type {string} * @type {string}
*/ */
public readonly regularPrice: string = ''; public readonly regularPrice: string = '';
/**
* Indicates that only one of a product may be held in the order at a time.
*
* @type {boolean}
*/
public readonly onePerOrder: boolean = false;
/**
* The taxability of the product.
*
* @type {Taxability}
*/
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
/**
* The tax class of the product
*
* @type {string}
*/
public readonly taxClass: string = '';
/**
* Indicates whether or not the product is currently on sale.
*
* @type {boolean}
*/
public readonly onSale: boolean = false;
/**
* The price of the product when on sale.
*
* @type {string}
*/
public readonly salePrice: string = '';
/**
* The GMT datetime when the product should start to be on sale.
*
* @type {Date|null}
*/
public readonly saleStart: Date | null = null;
/**
* The GMT datetime when the product should no longer be on sale.
*
* @type {Date|null}
*/
public readonly saleEnd: Date | null = null;
/**
* Indicates whether or not the product is downloadable.
*
* @type {boolean}
*/
public readonly isDownloadable: boolean = false;
/**
* The downloads available for the product.
*
* @type {ReadonlyArray.<ProductDownload>}
*/
public readonly downloads: readonly ProductDownload[] = [];
/**
* The maximum number of times a customer may download the product's contents.
*
* @type {number}
*/
public readonly downloadLimit: number = -1;
/**
* The number of days after purchase that a customer may still download the product's contents.
*
* @type {number}
*/
public readonly daysToDownload: number = -1;
/**
* The weight of the product in the store's current units.
*
* @type {string}
*/
public readonly weight: string = '';
/**
* The length of the product in the store's current units.
*
* @type {string}
*/
public readonly length: string = '';
/**
* The width of the product in the store's current units.
*
* @type {string}
*/
public readonly width: string = '';
/**
* The height of the product in the store's current units.
*
* @type {string}
*/
public readonly height: string = '';
/**
* Indicates that the product must be shipped.
*
* @type {boolean}
*/
public readonly requiresShipping: boolean = false;
/**
* Indicates that the product's shipping is taxable.
*
* @type {boolean}
*/
public readonly isShippingTaxable: boolean = false;
/**
* The shipping class for the product.
*
* @type {string}
*/
public readonly shippingClass: string = '';
/**
* Indicates that a product should use the inventory system.
*
* @type {boolean}
*/
public readonly trackInventory: boolean = false;
/**
* The number of inventory units remaining for this product.
*
* @type {number}
*/
public readonly remainingStock: number = -1;
/**
* The product's stock status.
*
* @type {StockStatus}
*/
public readonly stockStatus: StockStatus = ''
/**
* The status of backordering for a product.
*
* @type {BackorderStatus}
*/
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
/**
* Indicates whether or not a product can be backordered.
*
* @type {boolean}
*/
public readonly canBackorder: boolean = false;
/**
* Indicates whether or not a product is on backorder.
*
* @type {boolean}
*/
public readonly isOnBackorder: boolean = false;
/**
* Indicates whether or not a product allows reviews.
*
* @type {boolean}
*/
public readonly allowReviews: boolean = false;
/**
* The average rating for the product.
*
* @type {number}
*/
public readonly averageRating: number = -1;
/**
* The number of ratings for the product.
*
* @type {number}
*/
public readonly numRatings: number = -1;
/**
* The extra metadata for the product.
*
* @type {ReadonlyArray.<MetaData>}
*/
public readonly metaData: readonly MetaData[] = [];
} }

View File

@ -0,0 +1,261 @@
/**
* An enum describing the catalog visibility options for products.
*
* @enum {string}
*/
export enum CatalogVisibility {
/**
* The product should be visible everywhere.
*/
Everywhere = 'visible',
/**
* The product should only be visible in the shop catalog.
*/
ShopOnly = 'catalog',
/**
* The product should only be visible in search results.
*/
SearchOnly = 'search',
/**
* The product should be hidden everywhere.
*/
Hidden = 'hidden'
}
/**
* Indicates the taxability of a product.
*
* @enum {string}
*/
export enum Taxability {
/**
* The product and shipping are both taxable.
*/
ProductAndShipping = 'taxable',
/**
* Only the product's shipping is taxable.
*/
ShippingOnly = 'shipping',
/**
* The product and shipping are not taxable.
*/
None = 'none'
}
/**
* Indicates the status for backorders for a product.
*
* @enum {string}
*/
export enum BackorderStatus {
/**
* The product is allowed to be backordered.
*/
Allowed = 'yes',
/**
* The product is allowed to be backordered but it will notify the customer of that fact.
*/
AllowedWithNotification = 'notify',
/**
* The product is not allowed to be backordered.
*/
NotAllowed = 'no'
}
/**
* A product's stock status.
*
* @typedef StockStatus
* @alias 'instock'|'outofstock'|'onbackorder'|string
*/
export type StockStatus = 'instock' | 'outofstock' | 'onbackorder' | string
/**
* A products taxonomy term such as categories or tags.
*/
export class ProductTerm {
/**
* The ID of the term.
*
* @type {number}
*/
public readonly id: number = -1;
/**
* The name of the term.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The slug of the term.
*
* @type {string}
*/
public readonly slug: string = '';
/**
* Creates a new product term.
*
* @param {Partial.<ProductTerm>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductTerm > ) {
Object.assign( this, properties );
}
}
/**
* A product's download.
*/
export class ProductDownload {
/**
* The ID of the downloadable file.
*
* @type {string}
*/
public readonly id: string = '';
/**
* The name of the downloadable file.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The URL of the downloadable file.
*
*
* @type {string}
*/
public readonly url: string = '';
/**
* Creates a new product download.
*
* @param {Partial.<ProductDownload>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductDownload > ) {
Object.assign( this, properties );
}
}
/**
* A product's attributes.
*/
export class ProductAttribute {
/**
* The ID of the attribute.
*
* @type {number}
*/
public readonly id: number = -1;
/**
* The name of the attribute.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The sort order of the attribute.
*
* @type {number}
*/
public readonly sortOrder: number = -1;
/**
* Indicates whether or not the attribute is visible on the product page.
*
* @type {boolean}
*/
public readonly isVisibleOnProductPage: boolean = false;
/**
* Indicates whether or not the attribute should be used in variations.
*
* @type {boolean}
*/
public readonly isForVariations: boolean = false;
/**
* The options which are available for the attribute.
*
* @type {ReadonlyArray.<string>}
*/
public readonly options: readonly string[] = [];
/**
* Creates a new product attribute.
*
* @param {Partial.<ProductAttribute>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductAttribute > ) {
Object.assign( this, properties );
}
}
/**
* A product's image.
*/
export class ProductImage {
/**
* The ID of the image.
*
* @type {number}
*/
public readonly id: number = -1;
/**
* The GMT datetime when the image was created.
*
* @type {Date}
*/
public readonly created: Date = new Date();
/**
* The GMT datetime when the image was last modified.
*
* @type {Date}
*/
public readonly modified: Date = new Date();
/**
* The URL for the image file.
*
* @type {string}
*/
public readonly url: string = '';
/**
* The name of the image file.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The alt text to use on the image.
*
* @type {string}
*/
public readonly altText: string = '';
/**
* Creates a new product image.
*
* @param {Partial.<ProductImage>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductImage > ) {
Object.assign( this, properties );
}
}

View File

@ -1,13 +1,27 @@
import { AbstractProduct } from './abstract-product'; import { AbstractProduct, ProductSearchParams, ProductUpdateParams } from './abstract-product';
import { HTTPClient } from '../../http'; import { HTTPClient } from '../../http';
import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product'; import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product';
import { CreatesModels, ModelRepositoryParams } from '../../framework/model-repository'; import {
CreatesModels,
DeletesModels, ListsModels,
ModelRepositoryParams,
ReadsModels,
UpdatesModels,
} from '../../framework/model-repository';
/** /**
* The parameters embedded in this generic can be used in the ModelRepository in order to give * The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way. * type-safety in an incredibly granular way.
*/ */
export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, never, 'regularPrice' >; export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, ProductSearchParams, ProductUpdateParams >;
/**
* An interface for listing simple products using the repository.
*
* @typedef ListsSimpleProducts
* @alias ListsModels.<SimpleProduct>
*/
export type ListsSimpleProducts = ListsModels< SimpleProductRepositoryParams >;
/** /**
* An interface for creating simple products using the repository. * An interface for creating simple products using the repository.
@ -17,6 +31,30 @@ export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct
*/ */
export type CreatesSimpleProducts = CreatesModels< SimpleProductRepositoryParams >; export type CreatesSimpleProducts = CreatesModels< SimpleProductRepositoryParams >;
/**
* An interface for reading simple products using the repository.
*
* @typedef ReadsSimpleProducts
* @alias ReadsModels.<SimpleProduct>
*/
export type ReadsSimpleProducts = ReadsModels< SimpleProductRepositoryParams >;
/**
* An interface for updating simple products using the repository.
*
* @typedef UpdatesSimpleProducts
* @alias UpdatesModels.<SimpleProduct>
*/
export type UpdatesSimpleProducts = UpdatesModels< SimpleProductRepositoryParams >;
/**
* An interface for deleting simple products using the repository.
*
* @typedef DeletesSimpleProducts
* @alias DeletesModels.<SimpleProduct>
*/
export type DeletesSimpleProducts = DeletesModels< SimpleProductRepositoryParams >;
/** /**
* A simple product object. * A simple product object.
*/ */
@ -26,7 +64,7 @@ export class SimpleProduct extends AbstractProduct {
* *
* @param {Object} properties The properties to set in the object. * @param {Object} properties The properties to set in the object.
*/ */
public constructor( properties: Partial< SimpleProduct > = {} ) { public constructor( properties?: Partial< SimpleProduct > ) {
super(); super();
Object.assign( this, properties ); Object.assign( this, properties );
} }

View File

@ -47,7 +47,7 @@ export class SettingGroup extends Model {
* *
* @param {Object} properties The properties to set in the object. * @param {Object} properties The properties to set in the object.
*/ */
public constructor( properties: Partial< SettingGroup > = {} ) { public constructor( properties?: Partial< SettingGroup > ) {
super(); super();
Object.assign( this, properties ); Object.assign( this, properties );
} }

View File

@ -94,7 +94,7 @@ export class Setting extends Model {
* *
* @param {Object} properties The properties to set in the object. * @param {Object} properties The properties to set in the object.
*/ */
public constructor( properties: Partial< Setting > = {} ) { public constructor( properties?: Partial< Setting > ) {
super(); super();
Object.assign( this, properties ); Object.assign( this, properties );
} }

View File

@ -0,0 +1,47 @@
import { Model } from './model';
/**
* A constructor for a model.
*
* @typedef ModelConstructor
* @alias Function.<T>
* @template T
*/
export type ModelConstructor< T extends Model > = new ( properties: Partial< T > ) => T;
/**
* A post's status.
*
* @typedef PostStatus
* @alias 'draft'|'pending'|'private'|'publish'|string
*/
export type PostStatus = 'draft' | 'pending' | 'private' | 'publish' | string;
/**
* A metadata object.
*/
export class MetaData extends Model {
/**
* The key of the metadata.
*
* @type {string}
*/
public readonly key: string = '';
/**
* The value of the metadata.
*
* @type {*}
*/
public readonly value: any = '';
/**
* Creates a new metadata.
*
* @param {Partial.<MetaData>} properties The properties to set.
*/
public constructor( properties?: Partial< MetaData > ) {
super();
Object.assign( this, properties );
}
}

View File

@ -0,0 +1,247 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../http';
import { ModelTransformer } from '../../../framework/model-transformer';
import { DummyModel } from '../../../__test_data__/dummy-model';
import {
restCreate,
restDelete, restDeleteChild,
restList,
restListChild,
restRead,
restReadChild,
restUpdate,
restUpdateChild,
} from '../shared';
import { ModelRepositoryParams } from '../../../framework/model-repository';
import { Model } from '../../../models/model';
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
class DummyChildModel extends Model {
public childName: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
type DummyChildParams = ModelRepositoryParams< DummyChildModel, { parent: string }, { childSearch: string }, 'childName' >
describe( 'Shared REST Functions', () => {
let mockClient: MockProxy< HTTPClient >;
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
beforeEach( () => {
mockClient = mock< HTTPClient >();
mockTransformer = mock< ModelTransformer< any > >();
} );
it( 'restList', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'Test-1',
label: 'Test 1',
},
{
id: 'Test-2',
label: 'Test 2',
},
],
) );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restList< DummyModelParams >( () => 'test-url', DummyModel, mockClient, mockTransformer );
const result = await fn( { search: 'Test' } );
expect( result ).toHaveLength( 2 );
expect( result[ 0 ] ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( result[ 1 ] ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url', { search: 'Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-2', label: 'Test 2' } );
} );
it( 'restListChildren', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'Test-1',
label: 'Test 1',
},
{
id: 'Test-2',
label: 'Test 2',
},
],
) );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restListChild< DummyChildParams >(
( parent ) => 'test-url-' + parent.parent,
DummyChildModel,
mockClient,
mockTransformer,
);
const result = await fn( { parent: '123' }, { childSearch: 'Test' } );
expect( result ).toHaveLength( 2 );
expect( result[ 0 ] ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
expect( result[ 1 ] ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123', { childSearch: 'Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-2', label: 'Test 2' } );
} );
it( 'restCreate', async () => {
mockClient.post.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restCreate< DummyModelParams >(
( properties ) => 'test-url-' + properties.name,
DummyModel,
mockClient,
mockTransformer,
);
const result = await fn( { name: 'Test' } );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { name: 'Test' } );
expect( mockClient.post ).toHaveBeenCalledWith( 'test-url-Test', { name: 'From-Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restRead', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restRead< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
const result = await fn( 123 );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restReadChildren', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restReadChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
DummyChildModel,
mockClient,
mockTransformer,
);
const result = await fn( { parent: '123' }, 456 );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123-456' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restUpdate', async () => {
mockClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restUpdate< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
const result = await fn( 123, { name: 'Test' } );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { name: 'Test' } );
expect( mockClient.patch ).toHaveBeenCalledWith( 'test-url-123', { name: 'From-Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restUpdateChildren', async () => {
mockClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restUpdateChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
DummyChildModel,
mockClient,
mockTransformer,
);
const result = await fn( { parent: '123' }, 456, { childName: 'Test' } );
expect( result ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { childName: 'Test' } );
expect( mockClient.patch ).toHaveBeenCalledWith( 'test-url-123-456', { name: 'From-Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restDelete', async () => {
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const fn = restDelete< DummyModelParams >( ( id ) => 'test-url-' + id, mockClient );
const result = await fn( 123 );
expect( result ).toBe( true );
expect( mockClient.delete ).toHaveBeenCalledWith( 'test-url-123' );
} );
it( 'restDeleteChildren', async () => {
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const fn = restDeleteChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
mockClient,
);
const result = await fn( { parent: '123' }, 456 );
expect( result ).toBe( true );
expect( mockClient.delete ).toHaveBeenCalledWith( 'test-url-123-456' );
} );
} );

View File

@ -1,28 +0,0 @@
import { simpleProductRESTRepository } from '../simple-product';
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { SimpleProduct } from '../../../../models';
describe( 'simpleProductRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof simpleProductRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = simpleProductRESTRepository( httpClient );
} );
it( 'should create', async () => {
httpClient.post.mockResolvedValue( new HTTPResponse(
200,
{},
{ id: 123 },
) );
const created = await repository.create( { name: 'Test Product' } );
expect( created ).toBeInstanceOf( SimpleProduct );
expect( created ).toMatchObject( { id: 123 } );
expect( httpClient.post ).toHaveBeenCalledWith( '/wc/v3/products', { type: 'simple', name: 'Test Product' } );
} );
} );

View File

@ -0,0 +1,221 @@
import { ModelTransformation, ModelTransformer, TransformationOrder } from '../../../framework/model-transformer';
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
import { AbstractProduct } from '../../../models/products/abstract-product';
import { AddPropertyTransformation } from '../../../framework/transformations/add-property-transformation';
import { IgnorePropertyTransformation } from '../../../framework/transformations/ignore-property-transformation';
import {
PropertyType,
PropertyTypeTransformation,
} from '../../../framework/transformations/property-type-transformation';
import { CustomTransformation } from '../../../framework/transformations/custom-transformation';
import { ProductAttribute, ProductDownload, ProductImage, ProductTerm } from '../../../models/products/shared-types';
import { ModelTransformerTransformation } from '../../../framework/transformations/model-transformer-transformation';
import { MetaData } from '../../../models/shared-types';
import { createMetaDataTransformer } from '../shared';
/**
* Creates a transformer for the product term object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductTermTransformer(): ModelTransformer< ProductTerm > {
return new ModelTransformer(
[
new PropertyTypeTransformation( { id: PropertyType.Integer } ),
],
);
}
/**
* Creates a transformer for the product attribute object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductAttributeTransformer(): ModelTransformer< ProductAttribute > {
return new ModelTransformer(
[
new PropertyTypeTransformation(
{
id: PropertyType.Integer,
sortOrder: PropertyType.Integer,
isVisibleOnProductPage: PropertyType.Boolean,
isForVariations: PropertyType.Boolean,
},
),
new KeyChangeTransformation< ProductAttribute >(
{
sortOrder: 'position',
isVisibleOnProductPage: 'visible',
isForVariations: 'variation',
},
),
],
);
}
/**
* Creates a transformer for the product image object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductImageTransformer(): ModelTransformer< ProductImage > {
return new ModelTransformer(
[
new IgnorePropertyTransformation( [ 'date_created', 'date_modified' ] ),
new PropertyTypeTransformation(
{
id: PropertyType.Integer,
created: PropertyType.Date,
modified: PropertyType.Date,
},
),
new KeyChangeTransformation< ProductImage >(
{
created: 'date_created_gmt',
modified: 'date_modified_gmt',
url: 'src',
altText: 'altText',
},
),
],
);
}
/**
* Creates a transformer for the product download object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductDownloadTransformer(): ModelTransformer< ProductDownload > {
return new ModelTransformer(
[
new KeyChangeTransformation< ProductDownload >( { url: 'file' } ),
],
);
}
/**
* Creates a transformer for the shared properties of all products.
*
* @param {string} type The product type.
* @param {Array.<ModelTransformation>} transformations Optional transformers to add to the transformer.
* @return {ModelTransformer} The created transformer.
*/
export function createProductTransformer< T extends AbstractProduct >(
type: string,
transformations?: ModelTransformation[],
): ModelTransformer< T > {
if ( ! transformations ) {
transformations = [];
}
transformations.push(
new AddPropertyTransformation( {}, { type } ),
new IgnorePropertyTransformation(
[
'date_created',
'date_modified',
'date_on_sale_from',
'date_on_sale_to',
],
),
new ModelTransformerTransformation( 'categories', ProductTerm, createProductTermTransformer() ),
new ModelTransformerTransformation( 'tags', ProductTerm, createProductTermTransformer() ),
new ModelTransformerTransformation( 'attributes', ProductAttribute, createProductAttributeTransformer() ),
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
new ModelTransformerTransformation( 'downloads', ProductDownload, createProductDownloadTransformer() ),
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
new CustomTransformation(
TransformationOrder.Normal,
( properties: any ) => {
if ( properties.hasOwnProperty( 'dimensions' ) ) {
properties.length = properties.dimensions.length;
properties.width = properties.dimensions.width;
properties.height = properties.dimensions.height;
delete properties.dimensions;
}
return properties;
},
( properties: any ) => {
if ( properties.hasOwnProperty( 'length ' ) ||
properties.hasOwnProperty( 'width' ) ||
properties.hasOwnProperty( 'height' ) ) {
properties.dimensions = {
length: properties.length,
width: properties.width,
height: properties.height,
};
delete properties.length;
delete properties.width;
delete properties.height;
}
return properties;
},
),
new PropertyTypeTransformation(
{
created: PropertyType.Date,
modified: PropertyType.Date,
isPurchasable: PropertyType.Boolean,
isFeatured: PropertyType.Boolean,
isVirtual: PropertyType.Boolean,
onePerOrder: PropertyType.Boolean,
onSale: PropertyType.Boolean,
saleStart: PropertyType.Date,
saleEnd: PropertyType.Date,
isDownloadable: PropertyType.Boolean,
downloadLimit: PropertyType.Integer,
daysToDownload: PropertyType.Integer,
requiresShipping: PropertyType.Boolean,
isShippingTaxable: PropertyType.Boolean,
trackInventory: PropertyType.Boolean,
remainingStock: PropertyType.Integer,
canBackorder: PropertyType.Boolean,
isOnBackorder: PropertyType.Boolean,
allowReviews: PropertyType.Boolean,
averageRating: PropertyType.Integer,
numRatings: PropertyType.Integer,
},
),
new KeyChangeTransformation< AbstractProduct >(
{
created: 'date_created_gmt',
modified: 'date_modified_gmt',
postStatus: 'status',
shortDescription: 'short_description',
isPurchasable: 'purchasable',
isFeatured: 'featured',
isVirtual: 'virtual',
catalogVisibility: 'catalog_visibility',
regularPrice: 'regular_price',
onePerOrder: 'sold_individually',
taxStatus: 'tax_status',
taxClass: 'tax_class',
onSale: 'on_sale',
salePrice: 'sale_price',
saleStart: 'date_on_sale_from_gmt',
saleEnd: 'date_on_sale_to_gmt',
isDownloadable: 'downloadable',
downloadLimit: 'download_limit',
daysToDownload: 'download_expiry',
requiresShipping: 'shipping_required',
isShippingTaxable: 'shipping_taxable',
shippingClass: 'shipping_class',
trackInventory: 'manage_stock',
remainingStock: 'stock_quantity',
stockStatus: 'stock_status',
backorderStatus: 'backorders',
canBackorder: 'backorders_allowed',
isOnBackorder: 'backordered',
allowReviews: 'reviews_allowed',
averageRating: 'average_rating',
numRatings: 'rating_count',
metaData: 'meta_data',
},
),
);
return new ModelTransformer( transformations );
}

View File

@ -1,39 +1,43 @@
import { HTTPClient } from '../../../http'; import { HTTPClient } from '../../../http';
import { CreateFn, ModelRepository } from '../../../framework/model-repository'; import { ModelRepository } from '../../../framework/model-repository';
import { SimpleProduct } from '../../../models'; import { SimpleProduct } from '../../../models';
import { CreatesSimpleProducts, SimpleProductRepositoryParams } from '../../../models/products/simple-product'; import {
CreatesSimpleProducts,
function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProductRepositoryParams > { DeletesSimpleProducts,
return async ( properties ) => { ListsSimpleProducts,
const response = await httpClient.post( ReadsSimpleProducts,
'/wc/v3/products', SimpleProductRepositoryParams,
{ UpdatesSimpleProducts,
type: 'simple', } from '../../../models/products/simple-product';
name: properties.name, import { createProductTransformer } from './shared';
regular_price: properties.regularPrice, import { restCreate, restDelete, restList, restRead, restUpdate } from '../shared';
}, import { ModelID } from '../../../models/model';
);
return Promise.resolve( new SimpleProduct( {
id: response.data.id,
name: response.data.name,
regularPrice: response.data.regular_price,
} ) );
};
}
/** /**
* Creates a new ModelRepository instance for interacting with models via the REST API. * Creates a new ModelRepository instance for interacting with models via the REST API.
* *
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using. * @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
* @return {CreatesSimpleProducts} The created repository. * @return {
* ListsSimpleProducts|
* CreatesSimpleProducts|
* ReadsSimpleProducts|
* UpdatesSimpleProducts|
* DeletesSimpleProducts
* } The created repository.
*/ */
export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesSimpleProducts { export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimpleProducts
& CreatesSimpleProducts
& ReadsSimpleProducts
& UpdatesSimpleProducts
& DeletesSimpleProducts {
const buildURL = ( id: ModelID ) => '/wc/v3/products/' + id;
const transformer = createProductTransformer( 'simple' );
return new ModelRepository( return new ModelRepository(
null, restList< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
restCreate( httpClient ), restCreate< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
null, restRead< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
null, restUpdate< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
null, restDelete< SimpleProductRepositoryParams >( buildURL, httpClient ),
); );
} }

View File

@ -1,37 +0,0 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { settingGroupRESTRepository } from '../setting-group';
describe( 'settingGroupRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof settingGroupRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = settingGroupRESTRepository( httpClient );
} );
it( 'should list', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'group_1',
label: 'Test Group 1',
},
{
id: 'group_2',
label: 'Test Group 2',
},
],
) );
const list = await repository.list();
expect( list ).toHaveLength( 2 );
expect( list[ 0 ] ).toMatchObject( { id: 'group_1', label: 'Test Group 1' } );
expect( list[ 1 ] ).toMatchObject( { id: 'group_2', label: 'Test Group 2' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings' );
} );
} );

View File

@ -1,73 +0,0 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { settingRESTRepository } from '../setting';
describe( 'settingGroupRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof settingRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = settingRESTRepository( httpClient );
} );
it( 'should list', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'setting_1',
label: 'Test Setting 1',
},
{
id: 'setting_2',
label: 'Test Setting 2',
},
],
) );
const list = await repository.list( 'general' );
expect( list ).toHaveLength( 2 );
expect( list[ 0 ] ).toMatchObject( { id: 'setting_1', label: 'Test Setting 1' } );
expect( list[ 1 ] ).toMatchObject( { id: 'setting_2', label: 'Test Setting 2' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general' );
} );
it( 'should read', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'setting_1',
label: 'Test Setting',
},
) );
const read = await repository.read( 'general', 'setting_1' );
expect( read ).toMatchObject( { id: 'setting_1', label: 'Test Setting' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general/setting_1' );
} );
it( 'should update', async () => {
httpClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'setting_1',
label: 'Test Setting',
value: 'updated-value',
},
) );
const updated = await repository.update( 'general', 'setting_1', { value: 'test-value' } );
expect( updated ).toMatchObject( { id: 'setting_1', value: 'updated-value' } );
expect( httpClient.patch ).toHaveBeenCalledWith(
'/wc/v3/settings/general/setting_1',
{ value: 'test-value' },
);
} );
} );

View File

@ -1,24 +1,17 @@
import { HTTPClient } from '../../../http'; import { HTTPClient } from '../../../http';
import { ListFn, ModelRepository } from '../../../framework/model-repository'; import { ModelRepository } from '../../../framework/model-repository';
import { SettingGroup } from '../../../models'; import { SettingGroup } from '../../../models';
import { ListsSettingGroups, SettingGroupRepositoryParams } from '../../../models/settings/setting-group'; import { ListsSettingGroups, SettingGroupRepositoryParams } from '../../../models/settings/setting-group';
import { ModelTransformer } from '../../../framework/model-transformer';
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
import { restList } from '../shared';
function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParams > { function createTransformer(): ModelTransformer< SettingGroup > {
return async () => { return new ModelTransformer(
const response = await httpClient.get( '/wc/v3/settings' ); [
new KeyChangeTransformation< SettingGroup >( { parentID: 'parent_id' } ),
const list: SettingGroup[] = []; ],
for ( const raw of response.data ) { );
list.push( new SettingGroup( {
id: raw.id,
label: raw.label,
description: raw.description,
parentID: raw.parent_id,
} ) );
}
return Promise.resolve( list );
};
} }
/** /**
@ -28,8 +21,10 @@ function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParam
* @return {ListsSettingGroups} The created repository. * @return {ListsSettingGroups} The created repository.
*/ */
export function settingGroupRESTRepository( httpClient: HTTPClient ): ListsSettingGroups { export function settingGroupRESTRepository( httpClient: HTTPClient ): ListsSettingGroups {
const transformer = createTransformer();
return new ModelRepository( return new ModelRepository(
restList( httpClient ), restList< SettingGroupRepositoryParams >( () => '/wc/v3/settings', SettingGroup, httpClient, transformer ),
null, null,
null, null,
null, null,

View File

@ -1,10 +1,5 @@
import { HTTPClient } from '../../../http'; import { HTTPClient } from '../../../http';
import { import { ModelRepository, ParentID } from '../../../framework/model-repository';
ListChildFn,
ModelRepository,
ReadChildFn,
UpdateChildFn,
} from '../../../framework/model-repository';
import { Setting } from '../../../models'; import { Setting } from '../../../models';
import { import {
ListsSettings, ListsSettings,
@ -12,61 +7,12 @@ import {
SettingRepositoryParams, SettingRepositoryParams,
UpdatesSettings, UpdatesSettings,
} from '../../../models/settings/setting'; } from '../../../models/settings/setting';
import { ModelTransformer } from '../../../framework/model-transformer';
import { restListChild, restReadChild, restUpdateChild } from '../shared';
import { ModelID } from '../../../models/model';
function restList( httpClient: HTTPClient ): ListChildFn< SettingRepositoryParams > { function createTransformer(): ModelTransformer< Setting > {
return async ( parent ) => { return new ModelTransformer( [] );
const response = await httpClient.get( '/wc/v3/settings/' + parent );
const list: Setting[] = [];
for ( const raw of response.data ) {
list.push( new Setting( {
id: raw.id,
label: raw.label,
description: raw.description,
type: raw.type,
options: raw.options,
default: raw.default,
value: raw.value,
} ) );
}
return Promise.resolve( list );
};
}
function restRead( httpClient: HTTPClient ): ReadChildFn< SettingRepositoryParams > {
return async ( parent, id ) => {
const response = await httpClient.get( '/wc/v3/settings/' + parent + '/' + id );
return Promise.resolve( new Setting( {
id: response.data.id,
label: response.data.label,
description: response.data.description,
type: response.data.type,
options: response.data.options,
default: response.data.default,
value: response.data.value,
} ) );
};
}
function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryParams > {
return async ( parent, id, params ) => {
const response = await httpClient.patch(
'/wc/v3/settings/' + parent + '/' + id,
params,
);
return Promise.resolve( new Setting( {
id: response.data.id,
label: response.data.label,
description: response.data.description,
type: response.data.type,
options: response.data.options,
default: response.data.default,
value: response.data.value,
} ) );
};
} }
/** /**
@ -76,11 +22,14 @@ function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryP
* @return {ListsSettings|ReadsSettings|UpdatesSettings} The created repository. * @return {ListsSettings|ReadsSettings|UpdatesSettings} The created repository.
*/ */
export function settingRESTRepository( httpClient: HTTPClient ): ListsSettings & ReadsSettings & UpdatesSettings { export function settingRESTRepository( httpClient: HTTPClient ): ListsSettings & ReadsSettings & UpdatesSettings {
const buildURL = ( parent: ParentID< SettingRepositoryParams >, id: ModelID ) => '/wc/v3/settings/' + parent + '/' + id;
const transformer = createTransformer();
return new ModelRepository( return new ModelRepository(
restList( httpClient ), restListChild< SettingRepositoryParams >( ( parent ) => '/wc/v3/settings/' + parent, Setting, httpClient, transformer ),
null, null,
restRead( httpClient ), restReadChild< SettingRepositoryParams >( buildURL, Setting, httpClient, transformer ),
restUpdate( httpClient ), restUpdateChild< SettingRepositoryParams >( buildURL, Setting, httpClient, transformer ),
null, null,
); );
} }

View File

@ -0,0 +1,258 @@
import { ModelTransformer } from '../../framework/model-transformer';
import { MetaData, ModelConstructor } from '../../models/shared-types';
import { IgnorePropertyTransformation } from '../../framework/transformations/ignore-property-transformation';
import { HTTPClient } from '../../http';
import {
ListFn,
ModelRepositoryParams,
ModelClass,
HasParent,
ParentID,
ListChildFn,
ReadChildFn,
ReadFn,
DeleteFn,
UpdateFn,
UpdateChildFn,
DeleteChildFn,
CreateFn,
} from '../../framework/model-repository';
import { ModelID } from '../../models/model';
/**
* Creates a new transformer for metadata models.
*
* @return {ModelTransformer} The created transformer.
*/
export function createMetaDataTransformer(): ModelTransformer< MetaData > {
return new ModelTransformer(
[
new IgnorePropertyTransformation( [ 'id' ] ),
],
);
}
/**
* A callback to build a URL for a request.
*
* @callback BuildURLFn
* @param {ModelID} [id] The ID of the model we're dealing with if used for the request.
* @return {string} The URL to make the request to.
*/
type BuildURLFn< T extends ( 'list' | 'general' ) = 'general' > = [ T ] extends [ 'list' ] ? () => string : ( id: ModelID ) => string;
/**
* A callback to build a URL for a request.
*
* @callback BuildURLWithParentFn
* @param {P} parent The ID of the model's parent.
* @param {ModelID} [id] The ID of the model we're dealing with if used for the request.
* @return {string} The URL to make the request to.
* @template {ModelParentID} P
*/
type BuildURLWithParentFn< P extends ModelRepositoryParams, T extends ( 'list' | 'general' ) = 'general' > = [ T ] extends [ 'list' ]
? ( parent: ParentID< P > ) => string
: ( parent: ParentID< P >, id: ModelID ) => string;
/**
* Creates a callback for listing models using the REST API.
*
* @param {BuildURLFn} buildURL A callback to build the URL for the request.
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {ListFn} The callback for the repository.
*/
export function restList< T extends ModelRepositoryParams >(
buildURL: HasParent< T, never, BuildURLFn< 'list' > >,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): ListFn< T > {
return async ( params ) => {
const response = await httpClient.get( buildURL(), params );
const list: ModelClass< T >[] = [];
for ( const raw of response.data ) {
list.push( transformer.toModel( modelClass, raw ) );
}
return Promise.resolve( list );
};
}
/**
* Creates a callback for listing child models using the REST API.
*
* @param {BuildURLWithParentFn} buildURL A callback to build the URL for the request.
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {ListChildFn} The callback for the repository.
*/
export function restListChild< T extends ModelRepositoryParams >(
buildURL: HasParent< T, BuildURLWithParentFn< T, 'list' >, never >,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): ListChildFn< T > {
return async ( parent, params ) => {
const response = await httpClient.get( buildURL( parent ), params );
const list: ModelClass< T >[] = [];
for ( const raw of response.data ) {
list.push( transformer.toModel( modelClass, raw ) );
}
return Promise.resolve( list );
};
}
/**
* Creates a callback for creating models using the REST API.
*
* @param {Function} buildURL A callback to build the URL. (This is passed the properties for the new model.)
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {CreateFn} The callback for the repository.
*/
export function restCreate< T extends ModelRepositoryParams >(
buildURL: ( properties: Partial< ModelClass< T > > ) => string,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): CreateFn< T > {
return async ( properties ) => {
const response = await httpClient.post(
buildURL( properties ),
transformer.fromModel( properties ),
);
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
};
}
/**
* Creates a callback for reading models using the REST API.
*
* @param {BuildURLFn} buildURL A callback to build the URL for the request.
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {ReadFn} The callback for the repository.
*/
export function restRead< T extends ModelRepositoryParams >(
buildURL: HasParent< T, never, BuildURLFn >,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): ReadFn< T > {
return async ( id ) => {
const response = await httpClient.get( buildURL( id ) );
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
};
}
/**
* Creates a callback for reading child models using the REST API.
*
* @param {BuildURLWithParentFn} buildURL A callback to build the URL for the request.
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {ReadChildFn} The callback for the repository.
*/
export function restReadChild< T extends ModelRepositoryParams >(
buildURL: HasParent< T, BuildURLWithParentFn< T >, never >,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): ReadChildFn< T > {
return async ( parent, id ) => {
const response = await httpClient.get( buildURL( parent, id ) );
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
};
}
/**
* Creates a callback for updating models using the REST API.
*
* @param {BuildURLFn} buildURL A callback to build the URL for the request.
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {UpdateFn} The callback for the repository.
*/
export function restUpdate< T extends ModelRepositoryParams >(
buildURL: HasParent< T, never, BuildURLFn >,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): UpdateFn< T > {
return async ( id, params ) => {
const response = await httpClient.patch(
buildURL( id ),
transformer.fromModel( params as any ),
);
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
};
}
/**
* Creates a callback for updating child models using the REST API.
*
* @param {BuildURLWithParentFn} buildURL A callback to build the URL for the request.
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {UpdateChildFn} The callback for the repository.
*/
export function restUpdateChild< T extends ModelRepositoryParams >(
buildURL: HasParent< T, BuildURLWithParentFn< T >, never >,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): UpdateChildFn< T > {
return async ( parent, id, params ) => {
const response = await httpClient.patch(
buildURL( parent, id ),
transformer.fromModel( params as any ),
);
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
};
}
/**
* Creates a callback for deleting models using the REST API.
*
* @param {BuildURLFn} buildURL A callback to build the URL for the request.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @return {DeleteFn} The callback for the repository.
*/
export function restDelete< T extends ModelRepositoryParams >(
buildURL: HasParent< T, never, BuildURLFn >,
httpClient: HTTPClient,
): DeleteFn {
return ( id ) => {
return httpClient.delete( buildURL( id ) ).then( () => true );
};
}
/**
* Creates a callback for deleting child models using the REST API.
*
* @param {BuildURLWithParentFn} buildURL A callback to build the URL for the request.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @return {DeleteChildFn} The callback for the repository.
*/
export function restDeleteChild< T extends ModelRepositoryParams >(
buildURL: HasParent< T, BuildURLWithParentFn< T >, never >,
httpClient: HTTPClient,
): DeleteChildFn< T > {
return ( parent, id ) => {
return httpClient.delete( buildURL( parent, id ) ).then( () => true );
};
}

View File

@ -13,10 +13,10 @@ export function simpleProductFactory( httpClient ) {
return new AsyncFactory( return new AsyncFactory(
( { params } ) => { ( { params } ) => {
return new SimpleProduct( { return {
name: params.name ?? faker.commerce.productName(), name: params.name ?? faker.commerce.productName(),
regularPrice: params.regularPrice ?? faker.commerce.price(), regularPrice: params.regularPrice ?? faker.commerce.price(),
} ); };
}, },
( params ) => repository.create( params ), ( params ) => repository.create( params ),
); );