Skip to content

Commit

Permalink
Merge pull request #193 from ghidoz/#167-hasDirtyAttributes
Browse files Browse the repository at this point in the history
#167 has dirty attributes
  • Loading branch information
michDostal authored Sep 14, 2018
2 parents 3d66fdd + 9dad4f5 commit e732e41
Show file tree
Hide file tree
Showing 15 changed files with 3,356 additions and 553 deletions.
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

0 comments on commit e732e41

Please sign in to comment.