From cc58b739b867ae0f403f9e3b85026dd5c4122749 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Mon, 23 Oct 2017 10:09:30 +0200 Subject: [PATCH 1/8] Fix serializing params --- src/services/json-api-datastore.service.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index 92fdef6e..0a7b17cb 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -80,10 +80,8 @@ export class JsonApiDatastore { if (attributesMetadata.hasOwnProperty(propertyName)) { const metadata: any = attributesMetadata[propertyName]; - if (metadata.hasDirtyAttributes) { - const attributeName = metadata.serializedName != null ? metadata.serializedName : propertyName; - dirtyData[attributeName] = metadata.serialisationValue ? metadata.serialisationValue : metadata.newValue; - } + const attributeName = metadata.serializedName != null ? metadata.serializedName : propertyName; + dirtyData[attributeName] = metadata.serialisationValue ? metadata.serialisationValue : metadata.newValue; } } return dirtyData; @@ -339,7 +337,7 @@ export class JsonApiDatastore { } private toQueryString(params: any) { - return qs.stringify(params, { arrayFormat: 'brackets' }); + return qs.stringify(params, { arrayFormat: 'repeat' }); } public addToStore(modelOrModels: JsonApiModel | JsonApiModel[]): void { From 5f1a3fcda0dcc95b2f3fbbf771893c7e2b868dd1 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Mon, 23 Oct 2017 10:54:21 +0200 Subject: [PATCH 2/8] Remove attributes from relationships object --- src/services/json-api-datastore.service.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index 0a7b17cb..1780ff02 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -187,13 +187,21 @@ export class JsonApiDatastore { if (data.hasOwnProperty(key)) { if (data[key] instanceof JsonApiModel) { relationships = relationships || {}; - relationships[key] = { - data: this.buildSingleRelationshipData(data[key]) - }; + + if (data[key].id) { + relationships[key] = { + data: this.buildSingleRelationshipData(data[key]) + }; + } } else if (data[key] instanceof Array && data[key].length > 0 && this.isValidToManyRelation(data[key])) { relationships = relationships || {}; + + const relationshipData = data[key] + .filter((model: JsonApiModel) => model.id) + .map((model: JsonApiModel) => this.buildSingleRelationshipData(model)); + relationships[key] = { - data: data[key].map((model: JsonApiModel) => this.buildSingleRelationshipData(model)) + data: relationshipData }; } } From f15be33890c8c444f05862faa6f63ec2422bb0c1 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Mon, 23 Oct 2017 14:06:43 +0200 Subject: [PATCH 3/8] Add support for overriding internal methods --- README.MD | 9 +++++++-- src/index.ts | 2 +- src/interfaces/datastore-config.interface.ts | 5 ++++- src/interfaces/overrides.interface.ts | 3 +++ src/services/json-api-datastore.service.ts | 12 ++++++++---- 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 src/interfaces/overrides.interface.ts diff --git a/README.MD b/README.MD index 3d3629b9..2acac3de 100644 --- a/README.MD +++ b/README.MD @@ -391,7 +391,7 @@ If no MetadataModel is explicitly defined, the default one will be used, which j type: 'deals', meta: JsonApiMetaModel }) -export class Deal extends JsonApiModel +export class Deal extends JsonApiModel ``` The model itself is a normal class which contains the specific metadata properties. @@ -428,6 +428,11 @@ export class Datastore extends JsonApiDatastore { * `models` - all the models which will be stored in the datastore * `baseUrl` - base API URL * `apiVersion` - optional, a string which will be appended to the baseUrl +* `overrides` - used for overriding internal methods to achive custom functionalities + +##### Overrides + +* `getDirtyAttributes` - determines which model attributes are dirty ### Model config @@ -477,7 +482,7 @@ post.save({}, new Headers({'Authorization': 'Bearer ' + accessToken})).subscribe ### Error handling -Error handling is done in the `subscribe` method of the returned Observables. +Error handling is done in the `subscribe` method of the returned Observables. If your server returns valid [JSON API Error Objects](http://jsonapi.org/format/#error-objects) you can access them in your onError method: ```typescript diff --git a/src/index.ts b/src/index.ts index 20e75758..590f0e2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,10 @@ export * from './models/json-api.model'; export * from './models/error-response.model'; export * from './models/json-api-query-data'; +export * from './interfaces/overrides.interface'; export * from './interfaces/datastore-config.interface'; export * from './interfaces/model-config.interface'; export * from './providers'; export * from './module'; - diff --git a/src/interfaces/datastore-config.interface.ts b/src/interfaces/datastore-config.interface.ts index cbe413a9..bd6d3e77 100644 --- a/src/interfaces/datastore-config.interface.ts +++ b/src/interfaces/datastore-config.interface.ts @@ -1,5 +1,8 @@ +import { Overrides } from './overrides.interface'; + export interface DatastoreConfig { apiVersion?: string; baseUrl?: string; - models?: Object; + models?: object; + overrides?: Overrides; } diff --git a/src/interfaces/overrides.interface.ts b/src/interfaces/overrides.interface.ts new file mode 100644 index 00000000..2605b310 --- /dev/null +++ b/src/interfaces/overrides.interface.ts @@ -0,0 +1,3 @@ +export interface Overrides { + getDirtyAttributes?: (attributedMetadata: any) => object; +} diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index 1780ff02..e9b3063b 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -21,7 +21,9 @@ export class JsonApiDatastore { // tslint:disable:variable-name private _headers: Headers; private _store: {[type: string]: {[id: string]: JsonApiModel}} = {}; - + // tslint:disable-next-line:max-line-length + private getDirtyAttributes: Function = this.datastoreConfig.overrides && this.datastoreConfig.overrides.getDirtyAttributes ? this.datastoreConfig.overrides.getDirtyAttributes : this._getDirtyAttributes; + protected config: DatastoreConfig; constructor(private http: Http) {} @@ -73,15 +75,17 @@ export class JsonApiDatastore { return new modelType(this, { attributes: data }); } - private getDirtyAttributes(attributesMetadata: any): { string: any} { + private _getDirtyAttributes(attributesMetadata: any): { string: any} { const dirtyData: any = {}; for (const propertyName in attributesMetadata) { if (attributesMetadata.hasOwnProperty(propertyName)) { const metadata: any = attributesMetadata[propertyName]; - const attributeName = metadata.serializedName != null ? metadata.serializedName : propertyName; - dirtyData[attributeName] = metadata.serialisationValue ? metadata.serialisationValue : metadata.newValue; + if (metadata.hasDirtyAttributes) { + const attributeName = metadata.serializedName != null ? metadata.serializedName : propertyName; + dirtyData[attributeName] = metadata.serialisationValue ? metadata.serialisationValue : metadata.newValue; + } } } return dirtyData; From 1dc5b76ffa99b0ea7fe4d3e7bed929598325880d Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Mon, 23 Oct 2017 15:49:34 +0200 Subject: [PATCH 4/8] Adapt tests for relationship tweaks --- .../json-api-datastore.service.spec.ts | 41 ++++++++++++++++++- src/services/json-api-datastore.service.ts | 2 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/services/json-api-datastore.service.spec.ts b/src/services/json-api-datastore.service.spec.ts index 92d9b315..cedca6ad 100644 --- a/src/services/json-api-datastore.service.spec.ts +++ b/src/services/json-api-datastore.service.spec.ts @@ -255,6 +255,24 @@ describe('JsonApiDatastore', () => { }); datastore.query(Book, { arrayParam: [4, 5, 6] }).subscribe(); }); + + it('should generate correct query string for nested params with findAll', () => { + backend.connections.subscribe((c: MockConnection) => { + const decodedQueryString = decodeURI(c.request.url).split('?')[1]; + const expectedQueryString = 'filter[text]=test123'; + expect(decodedQueryString).toEqual(expectedQueryString); + }); + datastore.findAll(Book, { filter: { text: 'test123' } }).subscribe(); + }); + + it('should generate correct query string for nested array params with findAll', () => { + backend.connections.subscribe((c: MockConnection) => { + const decodedQueryString = decodeURI(c.request.url).split('?')[1]; + const expectedQueryString = 'filter[text][]=1&filter[text][]=2'; + expect(decodedQueryString).toEqual(expectedQueryString); + }); + datastore.findAll(Book, { filter: { text: [1, 2] } }).subscribe(); + }); }); describe('findRecord', () => { @@ -371,8 +389,26 @@ describe('JsonApiDatastore', () => { expect(obj.id).toBeUndefined(); expect(obj.type).toBe('authors'); expect(obj.relationships).toBeDefined(); + expect(obj.relationships.books.data.length).toBe(0); + }); + + const author = datastore.createRecord(Author, { + name: AUTHOR_NAME + }); + + author.books = [datastore.createRecord(Book, { + title: BOOK_TITLE + })]; + + author.save().subscribe(); + }); + + it('should create new author with new ToMany-relationship 2', () => { + backend.connections.subscribe((c: MockConnection) => { + const obj = c.request.json().data; + expect(obj.id).toBeUndefined(); + expect(obj.relationships).toBeDefined(); expect(obj.relationships.books.data.length).toBe(1); - expect(obj.relationships.books.data[0].attributes.title).toBe(BOOK_TITLE); }); const author = datastore.createRecord(Author, { @@ -380,7 +416,10 @@ describe('JsonApiDatastore', () => { }); author.books = [datastore.createRecord(Book, { + id: 123, title: BOOK_TITLE + }), datastore.createRecord(Book, { + title: `New book - ${BOOK_TITLE}` })]; author.save().subscribe(); diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index e9b3063b..b82457a6 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -349,7 +349,7 @@ export class JsonApiDatastore { } private toQueryString(params: any) { - return qs.stringify(params, { arrayFormat: 'repeat' }); + return qs.stringify(params, { arrayFormat: 'brackets' }); } public addToStore(modelOrModels: JsonApiModel | JsonApiModel[]): void { From 0ff272a0fd3f899911ef9d8378991c7524cedb48 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Mon, 23 Oct 2017 22:59:29 +0200 Subject: [PATCH 5/8] Add override for transforming query params --- README.MD | 1 + src/interfaces/overrides.interface.ts | 1 + src/services/json-api-datastore.service.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 2acac3de..6c15555a 100644 --- a/README.MD +++ b/README.MD @@ -433,6 +433,7 @@ export class Datastore extends JsonApiDatastore { ##### Overrides * `getDirtyAttributes` - determines which model attributes are dirty +* `toQueryString` - transforms query parameters to a query string ### Model config diff --git a/src/interfaces/overrides.interface.ts b/src/interfaces/overrides.interface.ts index 2605b310..2152e8f8 100644 --- a/src/interfaces/overrides.interface.ts +++ b/src/interfaces/overrides.interface.ts @@ -1,3 +1,4 @@ export interface Overrides { getDirtyAttributes?: (attributedMetadata: any) => object; + toQueryString?: (params: any) => string; } diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index b82457a6..eacaf3db 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -23,6 +23,7 @@ export class JsonApiDatastore { private _store: {[type: string]: {[id: string]: JsonApiModel}} = {}; // tslint:disable-next-line:max-line-length private getDirtyAttributes: Function = this.datastoreConfig.overrides && this.datastoreConfig.overrides.getDirtyAttributes ? this.datastoreConfig.overrides.getDirtyAttributes : this._getDirtyAttributes; + private toQueryString: Function = this.datastoreConfig.overrides && this.datastoreConfig.overrides.toQueryString ? this.datastoreConfig.overrides.toQueryString : this._toQueryString; protected config: DatastoreConfig; @@ -348,7 +349,7 @@ export class JsonApiDatastore { return new RequestOptions({ headers: requestHeaders }); } - private toQueryString(params: any) { + private _toQueryString(params: any): string { return qs.stringify(params, { arrayFormat: 'brackets' }); } From d6d7b0f0e6d7fc1a99337e9ff45d8a85878f7a96 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Mon, 23 Oct 2017 23:05:07 +0200 Subject: [PATCH 6/8] Disable tslint for max-line --- src/services/json-api-datastore.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index eacaf3db..28bbd15f 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -21,9 +21,10 @@ export class JsonApiDatastore { // tslint:disable:variable-name private _headers: Headers; private _store: {[type: string]: {[id: string]: JsonApiModel}} = {}; - // tslint:disable-next-line:max-line-length + // tslint:disable:max-line-length private getDirtyAttributes: Function = this.datastoreConfig.overrides && this.datastoreConfig.overrides.getDirtyAttributes ? this.datastoreConfig.overrides.getDirtyAttributes : this._getDirtyAttributes; private toQueryString: Function = this.datastoreConfig.overrides && this.datastoreConfig.overrides.toQueryString ? this.datastoreConfig.overrides.toQueryString : this._toQueryString; + // tslint:enable:max-line-length protected config: DatastoreConfig; From cbf26d7dad07bc7d9788c45a61e60de914c8ee10 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Tue, 24 Oct 2017 12:28:22 +0200 Subject: [PATCH 7/8] Fix saving model metadata --- src/constants/symbols.ts | 2 ++ src/decorators/attribute.decorator.ts | 32 +++++++++++++++++----- src/models/json-api.model.ts | 10 ++++--- src/services/json-api-datastore.service.ts | 5 ++-- 4 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 src/constants/symbols.ts diff --git a/src/constants/symbols.ts b/src/constants/symbols.ts new file mode 100644 index 00000000..1d314c43 --- /dev/null +++ b/src/constants/symbols.ts @@ -0,0 +1,2 @@ +// tslint:disable-next-line:variable-name +export const AttributeMetadata = Symbol('AttributeMetadata'); diff --git a/src/decorators/attribute.decorator.ts b/src/decorators/attribute.decorator.ts index 80dc51d1..81ddd281 100644 --- a/src/decorators/attribute.decorator.ts +++ b/src/decorators/attribute.decorator.ts @@ -1,4 +1,5 @@ import { format, parse } from 'date-fns'; +import { AttributeMetadata } from '../constants/symbols'; export function Attribute(serializedName?: string) { return function (target: any, propertyName: string) { @@ -16,10 +17,29 @@ export function Attribute(serializedName?: string) { return value; }; - const saveAnnotations = function (hasDirtyAttributes: boolean, oldValue: any, newValue: any, isNew: boolean) { - const annotations = Reflect.getMetadata('Attribute', target) || {}; + const saveAnnotations = function () { + const metadata = Reflect.getMetadata('Attribute', target) || {}; + + metadata[propertyName] = { + marked: true + }; + + Reflect.defineMetadata('Attribute', metadata, target); + }; + + const setMetadata = function ( + hasDirtyAttributes: boolean, + instance: any, + oldValue: any, + newValue: any, + isNew: boolean + ) { const targetType = Reflect.getMetadata('design:type', target, propertyName); + if (!instance[AttributeMetadata]) { + instance[AttributeMetadata] = {}; + } + const mappingMetadata = Reflect.getMetadata('AttributeMapping', target) || {}; const serializedPropertyName = serializedName !== undefined ? serializedName : propertyName; mappingMetadata[serializedPropertyName] = propertyName; @@ -27,15 +47,13 @@ export function Attribute(serializedName?: string) { const propertyHasDirtyAttributes = typeof oldValue === 'undefined' && !isNew ? false : hasDirtyAttributes; - annotations[propertyName] = { + instance[AttributeMetadata][propertyName] = { newValue, oldValue, serializedName, hasDirtyAttributes: propertyHasDirtyAttributes, serialisationValue: converter(targetType, newValue, true) }; - - Reflect.defineMetadata('Attribute', annotations, target); }; const getter = function () { @@ -47,13 +65,13 @@ export function Attribute(serializedName?: string) { const convertedValue = converter(targetType, newVal); if (convertedValue !== this['_' + propertyName]) { - saveAnnotations(true, this['_' + propertyName], newVal, !this.id); + setMetadata(true, this, this['_' + propertyName], newVal, !this.id); this['_' + propertyName] = convertedValue; } }; if (delete target[propertyName]) { - saveAnnotations(false, undefined, target[propertyName], target.id); + saveAnnotations(); Object.defineProperty(target, propertyName, { get: getter, set: setter, diff --git a/src/models/json-api.model.ts b/src/models/json-api.model.ts index 089db8f3..5721360a 100644 --- a/src/models/json-api.model.ts +++ b/src/models/json-api.model.ts @@ -5,6 +5,7 @@ import { Observable } from 'rxjs/Observable'; import { JsonApiDatastore, ModelType } from '../services/json-api-datastore.service'; import { ModelConfig } from '../interfaces/model-config.interface'; import * as _ from 'lodash'; +import { AttributeMetadata } from '../constants/symbols'; export class JsonApiModel { id: string; @@ -26,12 +27,12 @@ export class JsonApiModel { } save(params?: any, headers?: Headers): Observable { - const attributesMetadata: any = Reflect.getMetadata('Attribute', this); + const attributesMetadata: any = this[AttributeMetadata]; return this._datastore.saveRecord(attributesMetadata, this, params, headers); } get hasDirtyAttributes() { - const attributesMetadata: any = Reflect.getMetadata('Attribute', this); + const attributesMetadata: any = this[AttributeMetadata]; let hasDirtyAttributes = false; for (const propertyName in attributesMetadata) { if (attributesMetadata.hasOwnProperty(propertyName)) { @@ -47,7 +48,7 @@ export class JsonApiModel { } rollbackAttributes(): void { - const attributesMetadata: any = Reflect.getMetadata('Attribute', this); + const attributesMetadata: any = this[AttributeMetadata]; let metadata: any; for (const propertyName in attributesMetadata) { if (attributesMetadata.hasOwnProperty(propertyName)) { @@ -62,7 +63,8 @@ export class JsonApiModel { } } } - Reflect.defineMetadata('Attribute', attributesMetadata, this); + + this[AttributeMetadata] = attributesMetadata; } get modelConfig(): ModelConfig { diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index 92fdef6e..1418951b 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -13,6 +13,7 @@ import { JsonApiQueryData } from '../models/json-api-query-data'; import * as qs from 'qs'; import { DatastoreConfig } from '../interfaces/datastore-config.interface'; import { ModelConfig } from '../interfaces/model-config.interface'; +import { AttributeMetadata } from '../constants/symbols'; export type ModelType = { new(datastore: JsonApiDatastore, data: any): T; }; @@ -359,7 +360,7 @@ export class JsonApiDatastore { private resetMetadataAttributes(res: T, attributesMetadata: any, modelType: ModelType) { // TODO check why is attributesMetadata from the arguments never used // tslint:disable-next-line:no-param-reassign - attributesMetadata = Reflect.getMetadata('Attribute', res); + attributesMetadata = res[AttributeMetadata]; for (const propertyName in attributesMetadata) { if (attributesMetadata.hasOwnProperty(propertyName)) { @@ -371,7 +372,7 @@ export class JsonApiDatastore { } } - Reflect.defineMetadata('Attribute', attributesMetadata, res); + res[AttributeMetadata] = attributesMetadata; return res; } From ea2c601519a6060103dcb0db38ae16e9fe8d5da2 Mon Sep 17 00:00:00 2001 From: Mihael Safaric Date: Tue, 24 Oct 2017 14:33:00 +0200 Subject: [PATCH 8/8] Move saving attribute mappings to annotations --- src/decorators/attribute.decorator.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/decorators/attribute.decorator.ts b/src/decorators/attribute.decorator.ts index 81ddd281..31782fa2 100644 --- a/src/decorators/attribute.decorator.ts +++ b/src/decorators/attribute.decorator.ts @@ -25,6 +25,11 @@ export function Attribute(serializedName?: string) { }; Reflect.defineMetadata('Attribute', metadata, target); + + const mappingMetadata = Reflect.getMetadata('AttributeMapping', target) || {}; + const serializedPropertyName = serializedName !== undefined ? serializedName : propertyName; + mappingMetadata[serializedPropertyName] = propertyName; + Reflect.defineMetadata('AttributeMapping', mappingMetadata, target); }; const setMetadata = function ( @@ -40,11 +45,6 @@ export function Attribute(serializedName?: string) { instance[AttributeMetadata] = {}; } - const mappingMetadata = Reflect.getMetadata('AttributeMapping', target) || {}; - const serializedPropertyName = serializedName !== undefined ? serializedName : propertyName; - mappingMetadata[serializedPropertyName] = propertyName; - Reflect.defineMetadata('AttributeMapping', mappingMetadata, target); - const propertyHasDirtyAttributes = typeof oldValue === 'undefined' && !isNew ? false : hasDirtyAttributes; instance[AttributeMetadata][propertyName] = {