diff --git a/src/serde.spec.ts b/src/serde.spec.ts new file mode 100644 index 0000000..6ae32b2 --- /dev/null +++ b/src/serde.spec.ts @@ -0,0 +1,91 @@ +import { Exclude, Pluck, Serde } from './serde'; + +class ExcludeTestModel extends Serde { + name: string; + date: Date; + description: string; + @Exclude() frontendField: string; +} + +class PluckArrayTestModel extends Serde { + name: string; + @Pluck(['id']) nestedProperties: { id: number, name: string }[]; +} + +class PluckArrayTestTwoModel extends Serde { + name: string; + @Pluck('id') nestedProperties: { id: number, name: string }[]; +} + +class PluckObjectTestModel extends Serde { + name: string; + @Pluck(['id']) nestedProperty: { id: number, name: string }; +} + +class PluckObjectTestTwoModel extends Serde { + name: string; + @Pluck('id') nestedProperty: { id: number, name: string }; +} + +describe('Serde', () => { + + describe('@Exclude() decorator tests', () => { + it('should remove properties marked during serialize', () => { + const testModel = new ExcludeTestModel().deserialize({ + name: 'test model', + description: 'this is a test model', + frontendField: 'test field' + }); + const serializedModel = testModel.serialize(); + expect(serializedModel).toEqual({ name: 'test model', description: 'this is a test model' }); + }); + }); + + describe('@Pluck() decorator tests', () => { + it('should pluck \'id\' (property: T[]) from marked property during serialize', () => { + const testModel = new PluckArrayTestModel().deserialize({ + name: 'test model', + nestedProperties: [ + { id: 1, name: 'one' }, + { id: 2, name: 'two' } + ] + }); + + const serializedModel = testModel.serialize(); + expect(serializedModel).toEqual({ name: 'test model', nestedProperties: [{ id: 1 }, { id: 2 }] }); + }); + + it('should pluck \'ids\' (property: number[]) from marked property during serialize', () => { + const testModel = new PluckArrayTestTwoModel().deserialize({ + name: 'test model', + nestedProperties: [ + { id: 1, name: 'one' }, + { id: 2, name: 'two' } + ] + }); + + const serializedModel = testModel.serialize(); + expect(serializedModel).toEqual({ name: 'test model', nestedProperties: [1, 2] }); + }); + + it('should pluck \'id\' (property: {k: v}) from marked property during serialize', () => { + const testModel = new PluckObjectTestModel().deserialize({ + name: 'test model', + nestedProperty: { id: 2, name: 'two' } + }); + + const serializedModel = testModel.serialize(); + expect(serializedModel).toEqual({ name: 'test model', nestedProperty: { id: 2 } }); + }); + + it('should pluck \'id\' property: value from marked property during serialize', () => { + const testModel = new PluckObjectTestTwoModel().deserialize({ + name: 'test model', + nestedProperty: { id: 2, name: 'two' } + }); + + const serializedModel = testModel.serialize(); + expect(serializedModel).toEqual({ name: 'test model', nestedProperty: 2 }); + }); + }); +}); \ No newline at end of file diff --git a/src/serde.ts b/src/serde.ts new file mode 100644 index 0000000..101590b --- /dev/null +++ b/src/serde.ts @@ -0,0 +1,99 @@ +import "reflect-metadata"; + + +/* tslint:disable:comment-type */ +export const PLUCK_PROPERTIES_KEY = 'serde:pluck_properties'; +export const EXCLUDED_PROPERTIES_KEY = 'serde:excluded_properties'; + +export abstract class Serde extends Object { + protected removeableProperty(key: string): boolean { + return this.hasOwnProperty(key) && !(Reflect.getMetadata(EXCLUDED_PROPERTIES_KEY, this) || []).includes(key); + } + + protected recursiveSerialize(value: any) { + if (Array.isArray(value)) { + return value.map(v => v instanceof Serde ? v.serialize() : v); + } + return value instanceof Serde ? value.serialize() : value; + } + + copyWith(input: { [P in keyof Partial]: any }): this { + return Object.assign( + Object.create(Object.getPrototypeOf(this)), + this, + input + ); + } + + populate?(params: any): this; + + customSerialize?(input: { [P in keyof Partial]: any }): any; + + deserialize(input: Partial): this { + Object.assign(this, input); + return this; + } + + serialize() { + const pluckProperties = Reflect.getMetadata(PLUCK_PROPERTIES_KEY, this) as {[key: string]: any}; + return Object.entries(this) + .filter(([key, _]) => this.removeableProperty(key)) + .reduce((obj, [key, value]) => { + if (!!pluckProperties) { + const properties = pluckProperties[key]; + if (typeof properties !== 'undefined') { + if (Array.isArray(value)) { + obj[key] = Array.isArray(properties) ? + value.map(v => properties.reduce((nv, p) => ({ ...nv, [p]: v[p] }), {})) : + value.map(v => v[properties]); + } else { + obj[key] = Array.isArray(properties) ? + properties.reduce((nv, p) => ({ ...nv, [p]: value[p] }), {}) : + value[properties]; + } + return obj; + } + } + obj[key] = this.recursiveSerialize(value); + if (typeof this.customSerialize === 'function') { + obj = this.customSerialize(obj); + } + return obj; + }, {} as Partial); + } + +} + +/* tslint:disable:variable-name only-arrow-functions */ +/** + * Adding this decorator prevents the property from being included in the object built by Serde.serialize() + */ +export function Exclude(): Function { + return function (target: any, key: string): void { + Reflect.defineMetadata( + EXCLUDED_PROPERTIES_KEY, + [ + ...Reflect.getMetadata(EXCLUDED_PROPERTIES_KEY, target) || [], + key + ], + target + ); + }; +} + +/** + * Adding this decorator allows for plucking out an T[] or {T: v}[] + * + * @param field - use 'fieldName' when creating string|number[], + * use ['fieldName'] when creating {[fieldName]: value}[] + */ +export function Pluck(field: string | string[]): Function { + return function (target: any, key: string): void { + Reflect.defineMetadata(PLUCK_PROPERTIES_KEY, { + ...Reflect.getMetadata(PLUCK_PROPERTIES_KEY, target) || {}, + ...{ [key]: field } + }, target); + }; +} + +/* tslint:enable:variable-name */ \ No newline at end of file