Skip to content
6 changes: 6 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/constants/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// tslint:disable-next-line:variable-name
export const AttributeMetadata = Symbol('AttributeMetadata');
35 changes: 26 additions & 9 deletions src/decorators/attribute.decorator.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 () {
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion src/interfaces/datastore-config.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Overrides } from './overrides.interface';

export interface DatastoreConfig {
apiVersion?: string;
baseUrl?: string;
models?: Object;
models?: object;
overrides?: Overrides;
}
4 changes: 4 additions & 0 deletions src/interfaces/overrides.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Overrides {
getDirtyAttributes?: (attributedMetadata: any) => object;
toQueryString?: (params: any) => string;
}
10 changes: 6 additions & 4 deletions src/models/json-api.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,12 +27,12 @@ export class JsonApiModel {
}

save(params?: any, headers?: Headers): Observable<this> {
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)) {
Expand All @@ -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)) {
Expand All @@ -62,7 +63,8 @@ export class JsonApiModel {
}
}
}
Reflect.defineMetadata('Attribute', attributesMetadata, this);

this[AttributeMetadata] = attributesMetadata;
}

get modelConfig(): ModelConfig {
Expand Down
41 changes: 40 additions & 1 deletion src/services/json-api-datastore.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
Expand Down
31 changes: 22 additions & 9 deletions src/services/json-api-datastore.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends JsonApiModel> = { new(datastore: JsonApiDatastore, data: any): T; };

Expand All @@ -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) {}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
};
}
}
Expand Down Expand Up @@ -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' });
}

Expand All @@ -351,7 +364,7 @@ export class JsonApiDatastore {
private resetMetadataAttributes<T extends JsonApiModel>(res: T, attributesMetadata: any, modelType: ModelType<T>) {
// 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)) {
Expand All @@ -363,7 +376,7 @@ export class JsonApiDatastore {
}
}

Reflect.defineMetadata('Attribute', attributesMetadata, res);
res[AttributeMetadata] = attributesMetadata;
return res;
}

Expand Down