diff --git a/README.MD b/README.MD index fd3acbee..2bf9af5a 100644 --- a/README.MD +++ b/README.MD @@ -418,6 +418,12 @@ 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 +* `toQueryString` - transforms query parameters to a query string ### Model config 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 fc6a5819..e7e899d8 100644 --- a/src/decorators/attribute.decorator.ts +++ b/src/decorators/attribute.decorator.ts @@ -1,4 +1,4 @@ -import { format, parse } from 'date-fns'; +import { AttributeMetadata } from '../constants/symbols'; import { AttributeDecoratorOptions } from '../interfaces/attribute-decorator-options.interface'; import { DateConverter } from '../converters/date/date.converter'; @@ -30,26 +30,43 @@ export function Attribute(options: AttributeDecoratorOptions = {}): PropertyDeco return value; }; - const saveAnnotations = function (hasDirtyAttributes: boolean, oldValue: any, newValue: any, isNew: boolean) { - const annotations = Reflect.getMetadata('Attribute', target) || {}; - const targetType = Reflect.getMetadata('design:type', target, propertyName); + const saveAnnotations = function () { + const metadata = Reflect.getMetadata('Attribute', target) || {}; + + metadata[propertyName] = { + marked: true + }; + + Reflect.defineMetadata('Attribute', 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 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 propertyHasDirtyAttributes = typeof oldValue === 'undefined' && !isNew ? false : hasDirtyAttributes; - annotations[propertyName] = { + instance[AttributeMetadata][propertyName] = { newValue, oldValue, serializedName: options.serializedName, hasDirtyAttributes: propertyHasDirtyAttributes, serialisationValue: converter(targetType, newValue, true) }; - - Reflect.defineMetadata('Attribute', annotations, target); }; const getter = function () { @@ -61,13 +78,13 @@ export function Attribute(options: AttributeDecoratorOptions = {}): PropertyDeco 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/index.ts b/src/index.ts index d40e7aa1..dfd03c62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ 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 './interfaces/attribute-decorator-options.interface'; 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..2152e8f8 --- /dev/null +++ b/src/interfaces/overrides.interface.ts @@ -0,0 +1,4 @@ +export interface Overrides { + getDirtyAttributes?: (attributedMetadata: any) => object; + toQueryString?: (params: any) => string; +} 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.spec.ts b/src/services/json-api-datastore.service.spec.ts index 7de55b96..1b2b674d 100644 --- a/src/services/json-api-datastore.service.spec.ts +++ b/src/services/json-api-datastore.service.spec.ts @@ -256,6 +256,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', () => { @@ -364,14 +382,35 @@ 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, { name: AUTHOR_NAME }); 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 39a7465b..d5369f75 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; }; @@ -21,7 +22,11 @@ export class JsonApiDatastore { // tslint:disable:variable-name private _headers: Headers; private _store: {[type: string]: {[id: string]: JsonApiModel}} = {}; - + // 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; constructor(private http: Http) {} @@ -73,7 +78,7 @@ 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) { @@ -189,13 +194,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 }; } } @@ -330,7 +343,7 @@ export class JsonApiDatastore { return new RequestOptions({ headers: requestHeaders }); } - private toQueryString(params: any) { + private _toQueryString(params: any): string { return qs.stringify(params, { arrayFormat: 'brackets' }); } @@ -351,7 +364,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)) { @@ -363,7 +376,7 @@ export class JsonApiDatastore { } } - Reflect.defineMetadata('Attribute', attributesMetadata, res); + res[AttributeMetadata] = attributesMetadata; return res; }