Skip to content

Commit b52811e

Browse files
committed
Merge pull request #42 from pixelhandler/transform-attributes
Add transform methods for [de]serializing resource attributes
2 parents 37ecfab + 93ac60e commit b52811e

File tree

22 files changed

+311
-36
lines changed

22 files changed

+311
-36
lines changed

README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,17 @@ a specific specification for an API server without the need for an abstraction.
102102

103103
**Does this implement all of the JSON API specification?**
104104

105-
**Not yet**. The happy path for reading, creating, updating/patching, deleting
105+
**Most of it**. The happy path for reading, creating, updating/patching, deleting
106106
is ready, as well as patching relationships. No extension support has been worked
107107
on, e.g. [JSON Patch]. I would like to do that one day.
108108

109109
**Is this lightweight? Relative to what?**
110110

111111
**Yes**. With a server that follows the JSON API specification - it just works.
112-
This is a simple solution compared with starting from scratch using AJAX, or
113-
adapting Ember Data to work with the URLs. This solution provides a basic,
114-
timed caching solution to minimize requests, and leaves a more advanced caching
115-
strategy to the developer via a mixin. It does provide a `store` object that
116-
caches deserialized resources.
112+
This is a simple solution compared with starting from scratch using AJAX. This
113+
solution provides a basic, (timed) caching solution to minimize requests, and
114+
leaves a more advanced caching strategy to the developer via a mixin. It does
115+
provide a `store` object that caches deserialized resources.
117116

118117
**Are included resources supported (side-loading)?**
119118

@@ -259,11 +258,11 @@ Here is the blueprint for a `resource` (model) prototype:
259258

260259
```javascript
261260
import Ember from 'ember';
262-
import Resource from 'ember-jsonapi-resources/models/resource';
261+
import Resource from './resource';
263262
import { attr, hasOne, hasMany } from 'ember-jsonapi-resources/models/resource';
264263

265264
export default Resource.extend({
266-
type: '<%= entity %>',
265+
type: '<%= resource %>',
267266
service: Ember.inject.service('<%= resource %>'),
268267

269268
/*
@@ -281,7 +280,17 @@ export default Resource.extend({
281280
});
282281
```
283282

284-
The commented out code is an example of how to setup the relationships.
283+
The commented out code includes an example of how to setup the relationships
284+
and define your attributes. `attr()` can be used for any valid type, or you can
285+
specify a type, e.g. `attr('string')` or `attr('date')`. An attribute that is
286+
defined as a `'date'` type has a built in transform method to serialize and
287+
deserialize the date value. Typically the JSON value for a Date object is
288+
communicated in ISO format, e.g. "2015-08-25T22:05:37.393Z". The application
289+
serializer has methods for [de]serializing the date values between client and
290+
server. You can add your own transform methods based on the type of the
291+
attribute or based on the name of the attribute, the transform methods based on
292+
the name of the attribute will be called instead of any transform methods based
293+
on the type of the attribute.
285294

286295
The relationships are async using promise proxy objects. So when a template accesses
287296
the `resource`'s relationship a request is made for the relation.
@@ -310,6 +319,8 @@ var ENV = {
310319
};
311320
```
312321

322+
`MODEL_FACTORY_INJECTIONS` may be already set to `true` in the app/app.js file.
323+
313324
Also, once you've generated a `resource` you can assign the URL.
314325

315326
See this example: [tests/dummy/app/adapters/post.js](tests/dummy/app/adapters/post.js)
@@ -342,7 +353,9 @@ as needed to act as if the API server is running on your same domain.
342353

343354
#### Authorization
344355

345-
EJR by default will pick-up credentials saved on `localStorage['AuthorizationHeader']`. If you'd like to change this, for instance to make it work with `ember-simple-auth`, there's a [configurable mixin available](https://github.com/pixelhandler/ember-jsonapi-resources/wiki/Authorization).
356+
By default credentials stored at `localStorage['AuthorizationHeader']` will be used.
357+
If you'd like to change this, for instance to make it work with `ember-simple-auth`,
358+
there's a [configurable mixin available](https://github.com/pixelhandler/ember-jsonapi-resources/wiki/Authorization).
346359

347360
#### Example JSON API 1.0 Document
348361

addon/adapters/application.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export default Ember.Object.extend(Ember.Evented, {
231231
return resp.json().then(function(json) {
232232
if (isUpdate) {
233233
_this.cacheUpdate({ meta: json.meta, data: json.data, headers: resp.headers });
234+
json.data = _this.serializer.transformAttributes(json.data);
234235
resolve(json.data);
235236
} else {
236237
const resource = _this.serializer.deserialize(json);

addon/mixins/transforms.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
@module ember-jsonapi-resources
3+
@submodule transforms
4+
**/
5+
6+
import Ember from 'ember';
7+
import { dateTransform } from 'ember-jsonapi-resources/utils/transforms';
8+
9+
export default Ember.Mixin.create({
10+
/**
11+
@method serializeDateAttribute
12+
@param {Date|String} date
13+
@return {String|Null} date value as ISO String for JSON payload, or null
14+
*/
15+
serializeDateAttribute(date) {
16+
return dateTransform.serialize(date);
17+
},
18+
19+
/**
20+
@method deserializeDateAttribute
21+
@param {String} date usually in ISO format, must be a valid argument for Date
22+
@return {Date|Null} date value from JSON payload, or null
23+
*/
24+
deserializeDateAttribute(date) {
25+
return dateTransform.deserialize(date);
26+
}
27+
});

addon/serializers/application.js

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export default Ember.Object.extend({
3535
@return {Object}
3636
*/
3737
serializeResource(resource) {
38-
const json = resource.getProperties('type', 'attributes', 'relationships');
38+
let json = resource.getProperties('type', 'attributes', 'relationships');
39+
json = this.transformAttributes(json, 'serialize');
3940
for (let relationship in json.relationships) {
4041
if (json.relationships.hasOwnProperty(relationship)) {
4142
delete json.relationships[relationship].links;
@@ -57,13 +58,15 @@ export default Ember.Object.extend({
5758
serializeChanged(resource) {
5859
let json = resource.getProperties('id', 'type', 'changedAttributes');
5960
if (Ember.isEmpty(Object.keys(json.changedAttributes))) { return null; }
60-
return {
61+
let serialized = {
6162
data: {
6263
type: json.type,
6364
id: json.id,
6465
attributes: json.changedAttributes
6566
}
6667
};
68+
serialized.data = this.transformAttributes(serialized.data, 'serialize');
69+
return serialized;
6770
},
6871

6972
/**
@@ -105,6 +108,7 @@ export default Ember.Object.extend({
105108
@return {Resource}
106109
*/
107110
deserializeResource(json) {
111+
json = this.transformAttributes(json);
108112
return this._createResourceInstance(json);
109113
},
110114

@@ -127,6 +131,47 @@ export default Ember.Object.extend({
127131
}
128132
},
129133

134+
/**
135+
Transform attributes, serialize or deserialize by specified method or
136+
per type of attribute, e.g. date.
137+
138+
Your serializer may define a specific method for a type of attribute,
139+
i.e. `serializeDateAttribute` and/or `deserializeDateAttribute`. Likewise,
140+
your serializer may define a specific method for the name of an attribute,
141+
like `serializeUpdatedAtAttribute` and `deserializeUpdatedAtAttribute`.
142+
143+
During transformation a method based on the name of the attribute takes
144+
priority over a transform method based on the type of attribute, e.g. date.
145+
146+
@method transformAttributes
147+
@param {Object} json with attributes hash of resource properties to be transformed
148+
@param {String} [operation='deserialize'] perform a serialize or deserialize
149+
operation, the default operation is to deserialize when not passed
150+
@return {Object} json
151+
*/
152+
transformAttributes(json, operation = 'deserialize') {
153+
assertTranformOperation(operation);
154+
let transformMethod, factory, meta;
155+
for (let attr in json.attributes) {
156+
transformMethod = [operation, Ember.String.capitalize(attr), 'Attribute'].join('');
157+
if (typeof this[transformMethod] === 'function') {
158+
json.attributes[attr] = this[transformMethod](json.attributes[attr]);
159+
} else {
160+
try {
161+
factory = this._lookupFactory(json.type);
162+
meta = factory.metaForProperty(attr);
163+
transformMethod = [operation, Ember.String.capitalize(meta.type), 'Attribute'].join('');
164+
if (typeof this[transformMethod] === 'function') {
165+
json.attributes[attr] = this[transformMethod](json.attributes[attr]);
166+
}
167+
} catch (e) {
168+
continue; // metaForProperty has an assertion that may throw
169+
}
170+
}
171+
}
172+
return json;
173+
},
174+
130175
/**
131176
Create a Resource from a JSON API Resource Object
132177
@@ -151,7 +196,22 @@ export default Ember.Object.extend({
151196
delete resource[prop];
152197
}
153198
}
154-
let factoryName = 'model:' + json.type;
155-
return this.container.lookupFactory(factoryName).create(resource);
199+
return this._lookupFactory(json.type).create(resource);
200+
},
201+
202+
/**
203+
@private
204+
@method _lookupFactory
205+
@param {String} type
206+
@return {Function} factory for creating resource instances
207+
*/
208+
_lookupFactory(type) {
209+
return this.container.lookupFactory('model:' + type);
156210
}
157211
});
212+
213+
const tranformOperations = ['serialize', 'deserialize'];
214+
215+
function assertTranformOperation(operation) {
216+
Ember.assert(`${operation} is not a valid transform operation`, tranformOperations.indexOf(operation) > -1);
217+
}

addon/utils/transforms.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
@module ember-jsonapi-resources
3+
@submodule utils
4+
**/
5+
6+
import { isBlank, isType } from 'ember-jsonapi-resources/utils/is';
7+
8+
/**
9+
@class TransformDateAttribute
10+
*/
11+
class TransformDateAttribute {
12+
13+
/**
14+
@method serialize
15+
@param {Date|String} date
16+
@return {String|Null} date value as ISO String for JSON payload, or null
17+
*/
18+
serialize(date) {
19+
if (isBlank(date) || date === '') {
20+
date = null;
21+
} else if (isType('date', date)) {
22+
date = date.toISOString();
23+
} else if (isType('string', date)) {
24+
date = new Date(date);
25+
}
26+
return (date) ? date : null;
27+
}
28+
29+
/**
30+
@method serialize
31+
@param {String} date usually in ISO format, must be a valid argument for Date
32+
@return {Date|Null} date value from JSON payload, or null
33+
*/
34+
deserialize(date) {
35+
if (isBlank(date)) {
36+
date = null;
37+
} else if (isType('string', date) || isType('number', date)) {
38+
date = new Date(date);
39+
}
40+
return (date) ? date : null;
41+
}
42+
43+
}
44+
45+
export let dateTransform = new TransformDateAttribute();

app/mixins/transforms.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from 'ember-jsonapi-resources/mixins/transforms';

app/serializers/application.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
export { default } from 'ember-jsonapi-resources/serializers/application';
1+
import TransformsMixin from '../mixins/transforms';
2+
import ApplicationSerializer from 'ember-jsonapi-resources/serializers/application';
3+
4+
/**
5+
Serializer for a JSON API resource, combines the addon ApplicationSerializer and TransformsMixin
6+
7+
@class ApplicationSerializer
8+
@uses TransformsMixin
9+
*/
10+
export default ApplicationSerializer.extend(TransformsMixin);

fixtures/api/posts/1.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"slug": "practical-deploy-an-ember-app-with-ember-cli-deploy-on-digitalocean",
1111
"excerpt": "The notes below demonstrate how to setup a chat application, built with [Ember CLI], that uses a backend service from [Firebase].",
1212
"date": "2015-04-25",
13-
"body": "## Lightning-Approach Workflow\n\nThis approach is the default setup when using ember-cli-deploy and uses a Redis\nstore for the versions of your index.html file that you deploy."
13+
"body": "## Lightning-Approach Workflow\n\nThis approach is the default setup when using ember-cli-deploy and uses a Redis\nstore for the versions of your index.html file that you deploy.",
14+
"created-at": "2015-04-25T00:00:00.000Z",
15+
"updated-at": "2015-04-25T00:00:00.000Z"
1416
},
1517
"relationships": {
1618
"author": {

tests/dummy/app/adapters/post.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import ApplicationAdapter from './application';
22
import config from '../config/environment';
3+
import AuthorizationMixin from '../mixins/authorization';
34

4-
export default ApplicationAdapter.extend({
5+
export default ApplicationAdapter.extend(AuthorizationMixin, {
56
type: 'post',
67

78
url: config.APP.API_PATH + '/posts',

tests/dummy/app/models/author.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export default Resource.extend({
66
type: 'author',
77
service: Ember.inject.service('authors'),
88

9-
name: attr(),
10-
email: attr(),
9+
name: attr('string'),
10+
email: attr('string'),
1111

1212
posts: hasMany('posts')
1313
});

0 commit comments

Comments
 (0)