Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,318 changes: 2,793 additions & 525 deletions package-lock.json

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions src/converters/json-model/json-model.converter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { JsonModelConverter } from './json-model.converter';
import { School } from '../../../test/models/school.model';

describe('JsonModel converter', () => {
let converter: JsonModelConverter<any>;

describe('mask method', () => {

describe('ArrayModel - simple values', () => {
beforeEach(() => {
converter = new JsonModelConverter(Array);
});

it('should return empty array when empty input', () => {
const result = converter.mask(null);
expect(result instanceof Array).toBeTruthy();
});

it('should return the empty array when provided empty array', () => {
const DATA: Array<any> = [];
expect(converter.mask(DATA)).toEqual(DATA);
});

it('should return the array with data when provided default value', () => {
const DATA: Array<number> = [1, 2, 3];
expect(converter.mask(DATA)).toEqual(DATA);
});

it('should filter-out null values from array', () => {
const DATA: Array<number | null> = [1, 2, null, 3];
const result = converter.mask(DATA);
expect(result.length).toBe(3);
expect(result).not.toContain(null);
});
});

describe('Array model -> object values', () => {

beforeEach(() => {
converter = new JsonModelConverter(School);
});

it('should return array of Schools from provided data', () => {
const DATA: Array<any> = [
{ name: 'Massachusetts Institute of Technology', students: 11319, foundation: '1861-10-04' },
{ name: 'Charles University', students: 51438, foundation: '1348-04-07' },
];
const result = converter.mask(DATA);
expect(result.length).toBe(2);
expect(result[0]).toEqual(new School(DATA[0]));
expect(result[1]).toEqual(new School(DATA[1]));
});
});

describe('ObjectModel', () => {
beforeEach(() => {
converter = new JsonModelConverter(School);
});

it('should return new School when provided without value', () => {
const result = converter.mask(null);
expect(result instanceof School).toBeTruthy();
});

it('should not create new instance when already provided School instance', () => {
const DATA = new School({
name: 'Massachusetts Institute of Technology',
students: 11319,
foundation: '1861-10-04'
});
const result = converter.mask(DATA);
expect(result).toEqual(DATA);
});

it('should instance of School with data when provided initial data', () => {
const DATA = { name: 'Massachusetts Institute of Technology', students: 11319, foundation: '1861-10-04' };
const result: School = converter.mask(DATA);
expect(result.name).toBeTruthy(DATA.name);
expect(result.students).toBeTruthy(DATA.students);
expect(result.foundation).toBeTruthy(new Date(DATA.foundation));
});
});

describe('ObjectModel - nullable', () => {
beforeEach(() => {
converter = new JsonModelConverter(School, false);
});

it('should return null when null', () => {
const result = converter.mask(null);
expect(result instanceof School).toBeTruthy();
});
});
});

describe('unmask method', () => {
describe('ArrayModel - simple values', () => {
beforeEach(() => {
converter = new JsonModelConverter(Array);
});

it('should return serialized output when provided null', () => {
const result = converter.unmask(null);
expect(result).toBeNull();
});

it('should return serialized array of strings', () => {
const DATA: Array<any> = ['a', 'b', 'c'];
expect(converter.unmask(DATA)).toEqual(DATA);
});

it('should filter-out null values from array', () => {
const DATA: Array<number | null> = [1, 2, null, 3];
const result = converter.unmask(DATA);
expect(result.length).toBe(3);
expect(result).not.toContain(null);
});
});

describe('Array model -> object values', () => {

beforeEach(() => {
converter = new JsonModelConverter(School);
});

it('should return serialized Schools from provided Array of Schools', () => {
const DATA: Array<any> = [
{ name: 'Massachusetts Institute of Technology', students: 11319, foundation: '1861-10-04' },
{ name: 'Charles University', students: 51438, foundation: '1348-04-07' },
];
const schools: Array<School> = [new School(DATA[0]), new School(DATA[1])];
const result:Array<any> = converter.unmask(schools);
expect(result.length).toBe(2);
for (const key in result) {
expect(result[key].name).toBe(DATA[key].name);
expect(result[key].students).toBe(DATA[key].students);
expect(result[key].foundation).toContain(DATA[key].foundation);
}
});
});

describe('ObjectModel - nullable', () => {
beforeEach(() => {
converter = new JsonModelConverter(School, false);
});

it('should return null when null', () => {
const result = converter.unmask(null);
expect(result).toEqual(null);
});

it('should return serialized school when provided School instance', () => {
const DATA = { name: 'Massachusetts Institute of Technology', students: 11319, foundation: '1861-10-04' };
const result = converter.unmask(new School(DATA));
expect(result.name).toBe(DATA.name);
expect(result.students).toBe(DATA.students);
expect(result.foundation).toContain(DATA.foundation);
});
});
});
});
73 changes: 73 additions & 0 deletions src/converters/json-model/json-model.converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { JsonNestedApiModel, PropertyConverter } from '../..';

export class JsonModelConverter<T> implements PropertyConverter {
private modelType: any; // ModelType<T>

constructor(model: T, public nullValue: boolean = true) {
this.modelType = model; // <ModelType<T>>model
}

mask(value: any): T {
if (!value && !this.nullValue) {
return new this.modelType();
}

let result = null;
if (Array.isArray(value)) {
result = [];
for (const item of value) {
if (item === null) {
continue;
}
let temp;
if (typeof item === 'object') {
temp = new this.modelType();
temp.fill(item);
} else {
temp = item;
}

result.push(temp);
}
} else {
if (!(value instanceof this.modelType)) {
result = new this.modelType();
result.fill(value);
} else {
result = value;
}
}
return result;
}

unmask(value: any): any {
if (!value) {
return value;
}
let result = null;
if (Array.isArray(value)) {
result = [];
for (const item of value) {
if (!item) {
continue;
}
if (item instanceof JsonNestedApiModel) {
item.nestedDataSerialization = true;
result.push(item.serialize());
item.nestedDataSerialization = false;
} else {
result.push(item);
}
}
} else {
if (value instanceof JsonNestedApiModel) {
value.nestedDataSerialization = true;
result = value.serialize();
value.nestedDataSerialization = false;
} else {
result = value;
}
}
return result;
}
}
25 changes: 14 additions & 11 deletions src/decorators/attribute.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AttributeMetadata } from '../constants/symbols';
import { AttributeDecoratorOptions } from '../interfaces/attribute-decorator-options.interface';
import { DateConverter } from '../converters/date/date.converter';
import * as _ from 'lodash';

export function Attribute(options: AttributeDecoratorOptions = {}): PropertyDecorator {
return function (target: any, propertyName: string) {
Expand Down Expand Up @@ -45,25 +46,21 @@ export function Attribute(options: AttributeDecoratorOptions = {}): PropertyDeco
};

const setMetadata = function (
hasDirtyAttributes: boolean,
instance: any,
oldValue: any,
newValue: any,
isNew: boolean
newValue: any
) {
const targetType = Reflect.getMetadata('design:type', target, propertyName);

if (!instance[AttributeMetadata]) {
instance[AttributeMetadata] = {};
}

const propertyHasDirtyAttributes = (oldValue === newValue) ? false : hasDirtyAttributes;

instance[AttributeMetadata][propertyName] = {
newValue,
oldValue,
nested:false,
serializedName: options.serializedName,
hasDirtyAttributes: propertyHasDirtyAttributes,
hasDirtyAttributes: !_.isEqual(oldValue,newValue),
serialisationValue: converter(targetType, newValue, true)
};
};
Expand All @@ -75,11 +72,17 @@ export function Attribute(options: AttributeDecoratorOptions = {}): PropertyDeco
const setter = function (newVal: any) {
const targetType = Reflect.getMetadata('design:type', target, propertyName);
const convertedValue = converter(targetType, newVal);

if (convertedValue !== this['_' + propertyName]) {
setMetadata(true, this, this['_' + propertyName], newVal, !this.id);
this['_' + propertyName] = convertedValue;
let oldValue = undefined;
if (this.isModelInitialization() && this.id) {
oldValue = converter(targetType, newVal);
} else {
if (this[AttributeMetadata] && this[AttributeMetadata][propertyName]) {
oldValue = this[AttributeMetadata][propertyName]['oldValue'];
}
}

this['_' + propertyName] = convertedValue;
setMetadata(this, oldValue, convertedValue);
};

if (delete target[propertyName]) {
Expand Down
69 changes: 69 additions & 0 deletions src/decorators/json-attribute.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { AttributeDecoratorOptions } from '../interfaces/attribute-decorator-options.interface';
import { DateConverter } from '../converters/date/date.converter';
import * as _ from 'lodash';

export function JsonAttribute(options: AttributeDecoratorOptions = {}): PropertyDecorator {
return function (target: any, propertyName: string) {
const converter = function (dataType: any, value: any, forSerialisation = false): any {
let attrConverter;

if (options.converter) {
attrConverter = options.converter;
} else if (dataType === Date) {
attrConverter = new DateConverter();
} else {
const datatype = new dataType();

if (datatype.mask && datatype.unmask) {
attrConverter = datatype;
}
}

if (attrConverter) {
if (!forSerialisation) {
return attrConverter.mask(value);
}
return attrConverter.unmask(value);
}

return value;
};

const saveAnnotations = function () {
const metadata = Reflect.getMetadata('JsonAttribute', target) || {};

metadata[propertyName] = {
marked: true
};

Reflect.defineMetadata('JsonAttribute', metadata, target);

const mappingMetadata = Reflect.getMetadata('AttributeMapping', target) || {};
const serializedPropertyName = options.serializedName !== undefined ? options.serializedName : propertyName;
mappingMetadata[serializedPropertyName] = propertyName;
Reflect.defineMetadata('AttributeMapping', mappingMetadata, target);
};

const getter = function () {
if (this.nestedDataSerialization) {
return converter(Reflect.getMetadata('design:type', target, propertyName), this['_' + propertyName], true);
}
return this['_' + propertyName];
};

const setter = function (newVal: any) {
const targetType = Reflect.getMetadata('design:type', target, propertyName);
this['_' + propertyName] = converter(targetType, newVal);
};

if (delete target[propertyName]) {
saveAnnotations();
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
};
}
Loading