Skip to content

Commit ca285d7

Browse files
authored
Merge pull request #116 from ghidoz/feature/custom-property-converters
Add custom property converter
2 parents 98b240a + 0487e31 commit ca285d7

File tree

8 files changed

+81
-49
lines changed

8 files changed

+81
-49
lines changed

README.MD

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class Datastore extends JsonApiDatastore {
116116
Then set up your models:
117117
- Extend the `JsonApiModel` class
118118
- Decorate it with `@JsonApiModelConfig`, passing the `type`
119-
- Decorate the class properties with `@Attribute`: The serialized property name can optionally be specified by setting it as value of the annotation.
119+
- Decorate the class properties with `@Attribute`
120120
- Decorate the relationships attributes with `@HasMany` and `@BelongsTo`
121121
- (optional) Define your [Metadata](#metadata)
122122

@@ -435,6 +435,16 @@ export class Post extends JsonApiModel { }
435435
* `modelEndpointUrl` - if not specified, `type` will be used instead
436436
* `meta` - optional, metadata model
437437

438+
### Decorators
439+
440+
#### Model decorators
441+
442+
* `Attribute(options: AttributeDecoratorOptions)`
443+
444+
* `AttributeDecoratorOptions`:
445+
446+
* `converter`, optional, must implement `PropertyConverter` interface
447+
* `serializedName`, optional
438448

439449
### Custom Headers
440450

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { format, parse } from 'date-fns';
2+
import { PropertyConverter } from '../../interfaces/property-converter.interface';
3+
4+
export class DateConverter implements PropertyConverter {
5+
mask(value: any) {
6+
return parse(value);
7+
}
8+
9+
unmask(value: any) {
10+
return format(value, 'YYYY-MM-DDTHH:mm:ss[Z]');
11+
}
12+
}

src/decorators/attribute.decorator.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
import { format, parse } from 'date-fns';
2+
import { AttributeDecoratorOptions } from '../interfaces/attribute-decorator-options.interface';
3+
import { DateConverter } from '../converters/date/date.converter';
24

3-
export function Attribute(serializedName?: string) {
5+
export function Attribute(options: AttributeDecoratorOptions = {}): PropertyDecorator {
46
return function (target: any, propertyName: string) {
57
const converter = function (dataType: any, value: any, forSerialisation = false): any {
6-
if (!forSerialisation) {
7-
if (dataType === Date) {
8-
return parse(value);
9-
}
8+
let attrConverter;
9+
10+
if (options.converter) {
11+
attrConverter = options.converter;
12+
} else if (dataType === Date) {
13+
attrConverter = new DateConverter();
1014
} else {
11-
if (dataType === Date) {
12-
return format(value, 'YYYY-MM-DDTHH:mm:ss[Z]');
15+
const datatype = new dataType();
16+
17+
if (datatype.mask && datatype.unmask) {
18+
attrConverter = datatype;
19+
}
20+
}
21+
22+
if (attrConverter) {
23+
if (!forSerialisation) {
24+
return attrConverter.mask(value);
25+
} else {
26+
return attrConverter.unmask(value);
1327
}
1428
}
1529

@@ -21,7 +35,7 @@ export function Attribute(serializedName?: string) {
2135
const targetType = Reflect.getMetadata('design:type', target, propertyName);
2236

2337
const mappingMetadata = Reflect.getMetadata('AttributeMapping', target) || {};
24-
const serializedPropertyName = serializedName !== undefined ? serializedName : propertyName;
38+
const serializedPropertyName = options.serializedName !== undefined ? options.serializedName : propertyName;
2539
mappingMetadata[serializedPropertyName] = propertyName;
2640
Reflect.defineMetadata('AttributeMapping', mappingMetadata, target);
2741

@@ -30,7 +44,7 @@ export function Attribute(serializedName?: string) {
3044
annotations[propertyName] = {
3145
newValue,
3246
oldValue,
33-
serializedName,
47+
serializedName: options.serializedName,
3448
hasDirtyAttributes: propertyHasDirtyAttributes,
3549
serialisationValue: converter(targetType, newValue, true)
3650
};

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ export * from './models/json-api-query-data';
1111

1212
export * from './interfaces/datastore-config.interface';
1313
export * from './interfaces/model-config.interface';
14+
export * from './interfaces/attribute-decorator-options.interface';
15+
export * from './interfaces/property-converter.interface';
1416

1517
export * from './providers';
1618

1719
export * from './module';
18-
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { PropertyConverter } from './property-converter.interface';
2+
3+
export interface AttributeDecoratorOptions {
4+
serializedName?: string;
5+
converter?: PropertyConverter;
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface PropertyConverter {
2+
mask(value: any): any;
3+
unmask(value: any): any;
4+
}

src/services/json-api-datastore.service.spec.ts

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ describe('JsonApiDatastore', () => {
108108
backend.connections.subscribe((c: MockConnection) => {
109109
expect(c.request.url).not.toEqual(`${BASE_URL}/${API_VERSION}`);
110110
expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/` + 'authors?' +
111-
encodeURIComponent('page[size]') + '=10&' +
112-
encodeURIComponent('page[number]') + '=1&' +
113-
encodeURIComponent('include') + '=comments&' +
114-
encodeURIComponent('filter[title][keyword]') + '=Tolkien');
111+
encodeURIComponent('page[size]') + '=10&' +
112+
encodeURIComponent('page[number]') + '=1&' +
113+
encodeURIComponent('include') + '=comments&' +
114+
encodeURIComponent('filter[title][keyword]') + '=Tolkien');
115115
expect(c.request.method).toEqual(RequestMethod.Get);
116116
});
117117
datastore.query(Author, {
@@ -134,6 +134,7 @@ describe('JsonApiDatastore', () => {
134134
expect(c.request.headers.has('Authorization')).toBeTruthy();
135135
expect(c.request.headers.get('Authorization')).toBe('Bearer');
136136
});
137+
137138
datastore.query(Author, null, new Headers({ Authorization: 'Bearer' })).subscribe();
138139
});
139140

@@ -212,11 +213,13 @@ describe('JsonApiDatastore', () => {
212213

213214
it('should fire error', () => {
214215
const resp = {
215-
errors: [{
216-
code: '100',
217-
title: 'Example error',
218-
detail: 'detailed error Message'
219-
}]
216+
errors: [
217+
{
218+
code: '100',
219+
title: 'Example error',
220+
detail: 'detailed error Message'
221+
}
222+
]
220223
};
221224

222225
backend.connections.subscribe((c: MockConnection) => {
@@ -227,15 +230,13 @@ describe('JsonApiDatastore', () => {
227230
})
228231
));
229232
});
230-
datastore.query(Author).subscribe((authors) => fail('onNext has been called'),
231-
(response) => {
232-
expect(response).toEqual(jasmine.any(ErrorResponse));
233-
expect(response.errors.length).toEqual(1);
234-
expect(response.errors[0].code).toEqual(resp.errors[0].code);
235-
expect(response.errors[0].title).toEqual(resp.errors[0].title);
236-
expect(response.errors[0].detail).toEqual(resp.errors[0].detail);
237-
},
238-
() => fail('onCompleted has been called'));
233+
datastore.query(Author).subscribe((authors) => fail('onNext has been called'), (response) => {
234+
expect(response).toEqual(jasmine.any(ErrorResponse));
235+
expect(response.errors.length).toEqual(1);
236+
expect(response.errors[0].code).toEqual(resp.errors[0].code);
237+
expect(response.errors[0].title).toEqual(resp.errors[0].title);
238+
expect(response.errors[0].detail).toEqual(resp.errors[0].detail);
239+
}, () => fail('onCompleted has been called'));
239240
});
240241

241242
it('should generate correct query string for array params with findAll', () => {
@@ -268,7 +269,6 @@ describe('JsonApiDatastore', () => {
268269
})
269270
));
270271
});
271-
272272
datastore.findRecord(Author, '1').subscribe((author) => {
273273
expect(author).toBeDefined();
274274
expect(author.id).toBe(AUTHOR_ID);
@@ -314,12 +314,10 @@ describe('JsonApiDatastore', () => {
314314
})
315315
));
316316
});
317-
318317
const author = datastore.createRecord(Author, {
319318
name: AUTHOR_NAME,
320319
date_of_birth: AUTHOR_BIRTH
321320
});
322-
323321
author.save().subscribe((val) => {
324322
expect(val.id).toBeDefined();
325323
expect(val.id).toEqual('1');
@@ -331,11 +329,9 @@ describe('JsonApiDatastore', () => {
331329
expect(c.request.method).toEqual(RequestMethod.Post);
332330
c.mockRespond(new Response(new ResponseOptions({ status: 204 })));
333331
});
334-
335332
const author = datastore.createRecord(Author, {
336333
name: AUTHOR_NAME
337334
});
338-
339335
author.save().subscribe((val) => {
340336
expect(val).toBeDefined();
341337
});
@@ -351,16 +347,13 @@ describe('JsonApiDatastore', () => {
351347
expect(obj.relationships.books.data.length).toBe(1);
352348
expect(obj.relationships.books.data[0].id).toBe('10');
353349
});
354-
355350
const author = datastore.createRecord(Author, {
356351
name: AUTHOR_NAME
357352
});
358-
359353
author.books = [new Book(datastore, {
360354
id: '10',
361355
title: BOOK_TITLE
362356
})];
363-
364357
author.save().subscribe();
365358
});
366359

@@ -374,15 +367,12 @@ describe('JsonApiDatastore', () => {
374367
expect(obj.relationships.books.data.length).toBe(1);
375368
expect(obj.relationships.books.data[0].attributes.title).toBe(BOOK_TITLE);
376369
});
377-
378370
const author = datastore.createRecord(Author, {
379371
name: AUTHOR_NAME
380372
});
381-
382373
author.books = [datastore.createRecord(Book, {
383374
title: BOOK_TITLE
384375
})];
385-
386376
author.save().subscribe();
387377
});
388378

@@ -395,15 +385,12 @@ describe('JsonApiDatastore', () => {
395385
expect(obj.relationships).toBeDefined();
396386
expect(obj.relationships.author.data.id).toBe(AUTHOR_ID);
397387
});
398-
399388
const book = datastore.createRecord(Book, {
400389
title: BOOK_TITLE
401390
});
402-
403391
book.author = new Author(datastore, {
404392
id: AUTHOR_ID
405393
});
406-
407394
book.save().subscribe();
408395
});
409396
});
@@ -420,16 +407,15 @@ describe('JsonApiDatastore', () => {
420407
expect(obj.id).toBe(AUTHOR_ID);
421408
expect(obj.type).toBe('authors');
422409
expect(obj.relationships).toBeUndefined();
423-
});
424410

411+
});
425412
const author = new Author(datastore, {
426413
id: AUTHOR_ID,
427414
attributes: {
428415
date_of_birth: parse(AUTHOR_BIRTH),
429416
name: AUTHOR_NAME
430417
}
431418
});
432-
433419
author.name = 'Rowling';
434420
author.date_of_birth = parse('1965-07-31');
435421
author.save().subscribe();

test/models/author.model.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ export class Author extends JsonApiModel {
1313
@Attribute()
1414
name: string;
1515

16-
@Attribute('dob')
16+
@Attribute({
17+
serializedName: 'dob'
18+
})
1719
date_of_birth: Date;
1820

19-
@Attribute()
20-
date_of_death: Date;
21-
2221
@Attribute()
2322
created_at: Date;
2423

0 commit comments

Comments
 (0)