Serde class setup & tests
This commit is contained in:
91
src/serde.spec.ts
Normal file
91
src/serde.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Exclude, Pluck, Serde } from './serde';
|
||||||
|
|
||||||
|
class ExcludeTestModel extends Serde<ExcludeTestModel> {
|
||||||
|
name: string;
|
||||||
|
date: Date;
|
||||||
|
description: string;
|
||||||
|
@Exclude() frontendField: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluckArrayTestModel extends Serde<PluckArrayTestModel> {
|
||||||
|
name: string;
|
||||||
|
@Pluck(['id']) nestedProperties: { id: number, name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluckArrayTestTwoModel extends Serde<PluckArrayTestTwoModel> {
|
||||||
|
name: string;
|
||||||
|
@Pluck('id') nestedProperties: { id: number, name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluckObjectTestModel extends Serde<PluckObjectTestModel> {
|
||||||
|
name: string;
|
||||||
|
@Pluck(['id']) nestedProperty: { id: number, name: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluckObjectTestTwoModel extends Serde<PluckObjectTestTwoModel> {
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
99
src/serde.ts
Normal file
99
src/serde.ts
Normal file
@@ -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<S> 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<C extends S>(input: { [P in keyof Partial<C>]: any }): this {
|
||||||
|
return Object.assign(
|
||||||
|
Object.create(Object.getPrototypeOf(this)),
|
||||||
|
this,
|
||||||
|
input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
populate?(params: any): this;
|
||||||
|
|
||||||
|
customSerialize?<C extends S>(input: { [P in keyof Partial<C>]: any }): any;
|
||||||
|
|
||||||
|
deserialize(input: Partial<S>): 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<S>);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
Reference in New Issue
Block a user