From 97e48fbf7a2e0d2ad0e7af27927457a831592794 Mon Sep 17 00:00:00 2001 From: Markus Hennerbichler Date: Sun, 24 Sep 2017 18:05:18 +0200 Subject: [PATCH 1/3] 3.7.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d75a835..e966e267 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "angular2-jsonapi", - "version": "3.7.0", + "version": "3.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9f354cb7..4a12e356 100644 --- a/package.json +++ b/package.json @@ -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", From 7e43266ea0d7775b5b0a4d1c320b47325119ffb3 Mon Sep 17 00:00:00 2001 From: Markus Hennerbichler Date: Fri, 25 Aug 2017 13:56:35 +0100 Subject: [PATCH 2/3] Implement optional attribute names. Fixes #86 --- src/decorators/attribute.decorator.ts | 109 +++++++------- src/models/json-api.model.spec.ts | 15 -- .../json-api-datastore.service.spec.ts | 17 ++- src/services/json-api-datastore.service.ts | 30 +++- test/fixtures/author.fixture.ts | 133 +++++++++--------- test/models/author.model.ts | 4 +- 6 files changed, 169 insertions(+), 139 deletions(-) diff --git a/src/decorators/attribute.decorator.ts b/src/decorators/attribute.decorator.ts index 216663bd..c1b30902 100644 --- a/src/decorators/attribute.decorator.ts +++ b/src/decorators/attribute.decorator.ts @@ -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 - }); - } - }; } diff --git a/src/models/json-api.model.spec.ts b/src/models/json-api.model.spec.ts index 69583e80..e29b729f 100644 --- a/src/models/json-api.model.spec.ts +++ b/src/models/json-api.model.spec.ts @@ -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(); }); @@ -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) => { @@ -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) => { diff --git a/src/services/json-api-datastore.service.spec.ts b/src/services/json-api-datastore.service.spec.ts index 5ec0fe9f..ecbd7e85 100644 --- a/src/services/json-api-datastore.service.spec.ts +++ b/src/services/json-api-datastore.service.spec.ts @@ -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'; @@ -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(); @@ -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(); @@ -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(); }); }); diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index 3c1ee524..f8c891fc 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -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; } } } @@ -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); @@ -200,6 +201,11 @@ export class JsonApiDatastore { } } + private deserializeModel(modelType: ModelType, data: any) { + data.attributes = this.transformSerializedNamesToPropertyNames(modelType, data.attributes); + return new modelType(this, data); + } + private extractRecordData(res: Response, modelType: ModelType, model?: T): T { let body: any = res.json(); if (!body) { @@ -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); @@ -323,4 +329,22 @@ export class JsonApiDatastore { const configFromDecorator: DatastoreConfig = Reflect.getMetadata('JsonApiDatastoreConfig', this.constructor); return Object.assign(configFromDecorator, this.config); } + + private transformSerializedNamesToPropertyNames(modelType: ModelType, 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); + } + + + } diff --git a/test/fixtures/author.fixture.ts b/test/fixtures/author.fixture.ts index d2e373e0..79e6626f 100644 --- a/test/fixtures/author.fixture.ts +++ b/test/fixtures/author.fixture.ts @@ -1,4 +1,5 @@ -import {getSampleBook} from "./book.fixture"; +import {getSampleBook} from './book.fixture'; + export const AUTHOR_ID = '1'; export const AUTHOR_NAME = 'J. R. R. Tolkien'; export const AUTHOR_BIRTH = '1892-01-03'; @@ -12,72 +13,78 @@ export const BOOK_PUBLISHED = '1954-07-29'; export const CHAPTER_TITLE = 'The Return Journey'; export function getAuthorData(relationship?: string, total?: number): any { - let response: any = { - 'id': AUTHOR_ID, - 'type': 'authors', - 'attributes': { - 'name': AUTHOR_NAME, - 'date_of_birth': AUTHOR_BIRTH, - 'date_of_death': AUTHOR_DEATH, - 'created_at': AUTHOR_CREATED, - 'updated_at': AUTHOR_UPDATED - }, - 'relationships': { - 'books': {'links': {'self': '/v1/authors/1/relationships/books', 'related': '/v1/authors/1/books'}} - }, - 'links': {'self': '/v1/authors/1'} - }; - if (relationship && relationship.indexOf('books') !== -1) { - response.relationships.books.data = []; - for (let i = 1; i <= total; i++) { - response.relationships.books.data.push({ - 'id': '' + i, - 'type': 'books' - }); + let response: any = { + 'id': AUTHOR_ID, + 'type': 'authors', + 'attributes': { + 'name': AUTHOR_NAME, + 'dob': AUTHOR_BIRTH, + 'date_of_death': AUTHOR_DEATH, + 'created_at': AUTHOR_CREATED, + 'updated_at': AUTHOR_UPDATED + }, + 'relationships': { + 'books': {'links': {'self': '/v1/authors/1/relationships/books', 'related': '/v1/authors/1/books'}} + }, + 'links': {'self': '/v1/authors/1'} + }; + if (relationship && relationship.indexOf('books') !== -1) { + response.relationships.books.data = []; + if (total === undefined) { + total = 0; + } + for (let i = 1; i <= total; i++) { + response.relationships.books.data.push({ + 'id': '' + i, + 'type': 'books' + }); + } } - } - return response; + return response; }; export function getIncludedBooks(totalBooks: number, relationship?: string, totalChapters?: number): any[] { - let responseArray: any[] = []; - let chapterId = 0; - for (let i = 1; i <= totalBooks; i++) { - let book: any = getSampleBook(i, AUTHOR_ID); - if (relationship && relationship.indexOf('books.chapters') !== -1) { - book.relationships.chapters.data = []; - for (let ic = 1; ic <= totalChapters; ic++) { - chapterId++; - book.relationships.chapters.data.push({ - 'id': '' + chapterId, - 'type': 'chapters' - }); - responseArray.push({ - 'id': '' + chapterId, - 'type': 'chapters', - 'attributes': { - 'title': CHAPTER_TITLE, - 'ordering': chapterId, - 'created_at': '2016-10-01T12:54:32Z', - 'updated_at': '2016-10-01T12:54:32Z' - }, - 'relationships': { - 'book': { - 'links': { - 'self': '/v1/authors/288/relationships/book', - 'related': '/v1/authors/288/book' - }, - 'data': { - 'id': '' + i, - 'type': 'books' - } + let responseArray: any[] = []; + let chapterId = 0; + if (totalChapters === undefined) { + totalChapters = 0; + } + for (let i = 1; i <= totalBooks; i++) { + let book: any = getSampleBook(i, AUTHOR_ID); + if (relationship && relationship.indexOf('books.chapters') !== -1) { + book.relationships.chapters.data = []; + for (let ic = 1; ic <= totalChapters; ic++) { + chapterId++; + book.relationships.chapters.data.push({ + 'id': '' + chapterId, + 'type': 'chapters' + }); + responseArray.push({ + 'id': '' + chapterId, + 'type': 'chapters', + 'attributes': { + 'title': CHAPTER_TITLE, + 'ordering': chapterId, + 'created_at': '2016-10-01T12:54:32Z', + 'updated_at': '2016-10-01T12:54:32Z' + }, + 'relationships': { + 'book': { + 'links': { + 'self': '/v1/authors/288/relationships/book', + 'related': '/v1/authors/288/book' + }, + 'data': { + 'id': '' + i, + 'type': 'books' + } + } + }, + 'links': {'self': '/v1/authors/288'} + }); } - }, - 'links': {'self': '/v1/authors/288'} - }); - } + } + responseArray.push(book); } - responseArray.push(book); - } - return responseArray; + return responseArray; } diff --git a/test/models/author.model.ts b/test/models/author.model.ts index e2030261..70f4cd6c 100644 --- a/test/models/author.model.ts +++ b/test/models/author.model.ts @@ -7,14 +7,14 @@ import {PageMetaData} from "./page-meta-data"; @JsonApiModelConfig({ type: 'authors', - meta: PageMetaData + meta: PageMetaData, }) export class Author extends JsonApiModel { @Attribute() name: string; - @Attribute() + @Attribute('dob') date_of_birth: Date; @Attribute() From 22cf1a9b385ab992350a873e92e64d881132269e Mon Sep 17 00:00:00 2001 From: Markus Hennerbichler Date: Sun, 24 Sep 2017 19:30:33 +0200 Subject: [PATCH 3/3] Add description of feature to README --- README.MD | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.MD b/README.MD index e8e3a92b..220afa8d 100644 --- a/README.MD +++ b/README.MD @@ -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) @@ -134,8 +134,8 @@ export class Post extends JsonApiModel { @Attribute() content: string; - @Attribute() - created_at: Date; + @Attribute('created-at') + createdAt: Date; @HasMany() comments: Comment[];