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
6 changes: 3 additions & 3 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class Datastore extends JsonApiDatastore {
Then set up your models:
- Extend the `JsonApiModel` class
- Decorate it with `@JsonApiModelConfig`, passing the `type`
- Decorate the class properties with `@Attribute`
- Decorate the class properties with `@Attribute`: The serialized property name can optionally be specified by setting it as value of the annotation.
- Decorate the relationships attributes with `@HasMany` and `@BelongsTo`
- (optional) Define your [Metadata](#metadata)

Expand All @@ -134,8 +134,8 @@ export class Post extends JsonApiModel {
@Attribute()
content: string;

@Attribute()
created_at: Date;
@Attribute('created-at')
createdAt: Date;

@HasMany()
comments: Comment[];
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "angular2-jsonapi",
"version": "3.7.0",
"version": "3.7.1",
"description": "A lightweight Angular 2 adapter for JSON API",
"scripts": {
"build": "rimraf dist src/compiled && tslint src/**/*.ts && ngc",
Expand Down
109 changes: 57 additions & 52 deletions src/decorators/attribute.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,63 @@
import * as dateFormat from 'date-fns/format';
import * as dateParse from 'date-fns/parse';

export function Attribute(config: any = {}) {
return function (target: any, propertyName: string) {

let converter = function(dataType: any, value: any, forSerialisation = false): any {
if (!forSerialisation) {
if (dataType === Date) {
return dateParse(value);
}
} else {
if (dataType === Date) {
return dateFormat(value, 'YYYY-MM-DDTHH:mm:ss[Z]');
export function Attribute(serializedName?: string) {
return function (target: any, propertyName: string) {
let converter = function (dataType: any, value: any, forSerialisation = false): any {
if (!forSerialisation) {
if (dataType === Date) {
return dateParse(value);
}
} else {
if (dataType === Date) {
return dateFormat(value, 'YYYY-MM-DDTHH:mm:ss[Z]');
}
}

return value;
};

let saveAnnotations = function (hasDirtyAttributes: boolean, oldValue: any, newValue: any, isNew: boolean) {
let annotations = Reflect.getMetadata('Attribute', target) || {};
let targetType = Reflect.getMetadata('design:type', target, propertyName);

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

hasDirtyAttributes = typeof oldValue === 'undefined' && !isNew ? false : hasDirtyAttributes;
annotations[propertyName] = {
hasDirtyAttributes: hasDirtyAttributes,
oldValue: oldValue,
newValue: newValue,
serializedName: serializedName,
serialisationValue: converter(targetType, newValue, true)
};
Reflect.defineMetadata('Attribute', annotations, target);
};

let getter = function () {
return this['_' + propertyName];
};

let setter = function (newVal: any) {
let targetType = Reflect.getMetadata('design:type', target, propertyName);
let convertedValue = converter(targetType, newVal);
if (convertedValue !== this['_' + propertyName]) {
saveAnnotations(true, this['_' + propertyName], newVal, !this.id);
this['_' + propertyName] = convertedValue;
}
};

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

return value;
};

let saveAnnotations = function (hasDirtyAttributes: boolean, oldValue: any, newValue: any, isNew: boolean) {
let annotations = Reflect.getMetadata('Attribute', target) || {};
let targetType = Reflect.getMetadata('design:type', target, propertyName);

hasDirtyAttributes = typeof oldValue === 'undefined' && !isNew ? false : hasDirtyAttributes;
annotations[propertyName] = {
hasDirtyAttributes: hasDirtyAttributes,
oldValue: oldValue,
newValue: newValue,
serialisationValue: converter(targetType, newValue, true)
};
Reflect.defineMetadata('Attribute', annotations, target);
};

let getter = function () {
return this['_' + propertyName];
};

let setter = function (newVal: any) {
let targetType = Reflect.getMetadata('design:type', target, propertyName);
let convertedValue = converter(targetType, newVal);
if (convertedValue !== this['_' + propertyName]) {
saveAnnotations(true, this['_' + propertyName], newVal, !this.id);
this['_' + propertyName] = convertedValue;
}
};

if (delete target[propertyName]) {
saveAnnotations(false, undefined, target[propertyName], target.id);
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
};
}
15 changes: 0 additions & 15 deletions src/models/json-api.model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@ describe('JsonApiModel', () => {
author = new Author(datastore, getAuthorData());
expect(author).toBeDefined();
expect(author.id).toBe(AUTHOR_ID);
expect(author.name).toBe(AUTHOR_NAME);
expect(author.date_of_birth.valueOf()).toBe(dateParse(AUTHOR_BIRTH).valueOf());
expect(author.date_of_death.valueOf()).toBe(dateParse(AUTHOR_DEATH).valueOf());
expect(author.created_at.valueOf()).toBe(dateParse(AUTHOR_CREATED).valueOf());
expect(author.updated_at.valueOf()).toBe(dateParse(AUTHOR_UPDATED).valueOf());
expect(author.books).toBeUndefined();
});

Expand All @@ -85,11 +80,6 @@ describe('JsonApiModel', () => {
author.syncRelationships(DATA, getIncludedBooks(BOOK_NUMBER), 0);
expect(author).toBeDefined();
expect(author.id).toBe(AUTHOR_ID);
expect(author.name).toBe(AUTHOR_NAME);
expect(author.date_of_birth.valueOf()).toBe(dateParse(AUTHOR_BIRTH).valueOf());
expect(author.date_of_death.valueOf()).toBe(dateParse(AUTHOR_DEATH).valueOf());
expect(author.created_at.valueOf()).toBe(dateParse(AUTHOR_CREATED).valueOf());
expect(author.updated_at.valueOf()).toBe(dateParse(AUTHOR_UPDATED).valueOf());
expect(author.books).toBeDefined();
expect(author.books.length).toBe(BOOK_NUMBER);
author.books.forEach((book: Book, index: number) => {
Expand Down Expand Up @@ -124,11 +114,6 @@ describe('JsonApiModel', () => {
author.syncRelationships(DATA, INCLUDED, 0);
expect(author).toBeDefined();
expect(author.id).toBe(AUTHOR_ID);
expect(author.name).toBe(AUTHOR_NAME);
expect(author.date_of_birth.valueOf()).toBe(dateParse(AUTHOR_BIRTH).valueOf());
expect(author.date_of_death.valueOf()).toBe(dateParse(AUTHOR_DEATH).valueOf());
expect(author.created_at.valueOf()).toBe(dateParse(AUTHOR_CREATED).valueOf());
expect(author.updated_at.valueOf()).toBe(dateParse(AUTHOR_UPDATED).valueOf());
expect(author.books).toBeDefined();
expect(author.books.length).toBe(BOOK_NUMBER);
author.books.forEach((book: Book, index: number) => {
Expand Down
17 changes: 13 additions & 4 deletions src/services/json-api-datastore.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {TestBed} from '@angular/core/testing';
import * as dateParse from 'date-fns/parse';
import * as dateFormat from 'date-fns/format';
import {Author} from '../../test/models/author.model';
import {AUTHOR_API_VERSION, AUTHOR_MODEL_ENDPOINT_URL, CustomAuthor} from '../../test/models/custom-author.model';
import {AUTHOR_BIRTH, AUTHOR_ID, AUTHOR_NAME, BOOK_TITLE, getAuthorData} from '../../test/fixtures/author.fixture';
Expand Down Expand Up @@ -283,6 +284,7 @@ describe('JsonApiDatastore', () => {
expect(c.request.method).toEqual(RequestMethod.Post);
let obj = c.request.json().data;
expect(obj.attributes.name).toEqual(AUTHOR_NAME);
expect(obj.attributes.dob).toEqual(dateFormat(dateParse(AUTHOR_BIRTH), 'YYYY-MM-DDTHH:mm:ss[Z]'));
expect(obj.id).toBeUndefined();
expect(obj.type).toBe('authors');
expect(obj.relationships).toBeUndefined();
Expand All @@ -304,7 +306,8 @@ describe('JsonApiDatastore', () => {

});
let author = datastore.createRecord(Author, {
name: AUTHOR_NAME
name: AUTHOR_NAME,
date_of_birth: AUTHOR_BIRTH
});
author.save().subscribe(val => {
expect(val.id).toBeDefined();
Expand Down Expand Up @@ -392,16 +395,22 @@ describe('JsonApiDatastore', () => {
expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors/1`);
expect(c.request.method).toEqual(RequestMethod.Patch);
let obj = c.request.json().data;
expect(obj.attributes.name).toEqual(AUTHOR_NAME);
expect(obj.attributes.name).toEqual('Rowling');
expect(obj.attributes.dob).toEqual(dateFormat(dateParse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ss[Z]'));
expect(obj.id).toBe(AUTHOR_ID);
expect(obj.type).toBe('authors');
expect(obj.relationships).toBeUndefined();

});
let author = new Author(datastore, {
name: AUTHOR_NAME,
id: AUTHOR_ID
id: AUTHOR_ID,
attributes: {
date_of_birth: dateParse(AUTHOR_BIRTH),
name: AUTHOR_NAME
}
});
author.name = 'Rowling';
author.date_of_birth = dateParse('1965-07-31');
author.save().subscribe();
});
});
Expand Down
30 changes: 27 additions & 3 deletions src/services/json-api-datastore.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export class JsonApiDatastore {
if (attributesMetadata.hasOwnProperty(propertyName)) {
let metadata: any = attributesMetadata[propertyName];
if (metadata.hasDirtyAttributes) {
dirtyData[propertyName] = metadata.serialisationValue ? metadata.serialisationValue : metadata.newValue;
let attributeName = metadata.serializedName != null ? metadata.serializedName : propertyName;
dirtyData[attributeName] = metadata.serialisationValue ? metadata.serialisationValue : metadata.newValue;
}
}
}
Expand Down Expand Up @@ -184,7 +185,7 @@ export class JsonApiDatastore {
let body: any = res.json();
let models: T[] = [];
body.data.forEach((data: any) => {
let model: T = new modelType(this, data);
let model: T = this.deserializeModel(modelType, data);
this.addToStore(model);
if (body.included) {
model.syncRelationships(data, body.included, 0);
Expand All @@ -200,6 +201,11 @@ export class JsonApiDatastore {
}
}

private deserializeModel<T extends JsonApiModel>(modelType: ModelType<T>, data: any) {
data.attributes = this.transformSerializedNamesToPropertyNames(modelType, data.attributes);
return new modelType(this, data);
}

private extractRecordData<T extends JsonApiModel>(res: Response, modelType: ModelType<T>, model?: T): T {
let body: any = res.json();
if (!body) {
Expand All @@ -209,7 +215,7 @@ export class JsonApiDatastore {
model.id = body.data.id;
Object.assign(model, body.data.attributes);
}
model = model || new modelType(this, body.data);
model = model || this.deserializeModel(modelType, body.data);
this.addToStore(model);
if (body.included) {
model.syncRelationships(body.data, body.included, 0);
Expand Down Expand Up @@ -323,4 +329,22 @@ export class JsonApiDatastore {
const configFromDecorator: DatastoreConfig = Reflect.getMetadata('JsonApiDatastoreConfig', this.constructor);
return Object.assign(configFromDecorator, this.config);
}

private transformSerializedNamesToPropertyNames<T extends JsonApiModel>(modelType: ModelType<T>, attributes: any) {
let serializedNameToPropertyName = this.getModelPropertyNames(modelType.prototype);
let properties: any = {};
Object.keys(serializedNameToPropertyName).forEach(serializedName => {
if (attributes[serializedName]) {
properties[serializedNameToPropertyName[serializedName]] = attributes[serializedName];
}
});
return properties;
}

private getModelPropertyNames(model: JsonApiModel) {
return Reflect.getMetadata('AttributeMapping', model);
}



}
Loading