Skip to content

Commit 58b8da6

Browse files
committed
Merge pull request #94 from digitalsadhu/add_deserialization_hooks
Add deserialization hooks
2 parents ad186d5 + c017b97 commit 58b8da6

File tree

7 files changed

+413
-108
lines changed

7 files changed

+413
-108
lines changed

README.md

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,38 @@ module.exports = function (MyModel) {
275275

276276
##### function parameters
277277

278-
- `options` All config options set for the serialization process. See below.
278+
- `options` All config options set for the serialization process.
279279
- `callback` Callback to call with error or serialized records
280280

281+
## Custom Deserialization
282+
For occasions where you need greater control over the deserialization process, you can implement a custom deserialization function for each model as needed. This function will be used instead of the regular deserialization process.
283+
284+
#### example
285+
```js
286+
module.exports = function (MyModel) {
287+
MyModel.jsonApiDeserialize = function (options, callback) {
288+
// either return an error
289+
var err = new Error('Unable to deserialize record');
290+
err.status = 500;
291+
cb(err)
292+
293+
//or
294+
//options.data is the raw data
295+
//options.result needs to be populated with deserialization result
296+
options.result = options.data.data.attributes;
297+
298+
cb(null, options);
299+
}
300+
}
301+
```
302+
303+
##### function parameters
304+
305+
- `options` All config options set for the deserialization process.
306+
- `callback` Callback to call with error or serialized records
307+
308+
## The options object
309+
281310
###### `options.type`
282311
Resource type. Originally calculated from a models plural. Is used in the default
283312
serialization process to set the type property for each model in a jsonapi response.
@@ -367,10 +396,80 @@ This is the attributes settings as defined in the `attributes` configuration opt
367396
explained earlier. Use this in `beforeJsonApiSerialize` to make any model specific
368397
adjustments before serialization.
369398

370-
## Serialization Hooks
371-
For occasions when you don't want to fully implement serialization for a model manually but
372-
you need to manipulate the serialization process, you can use the serialization
373-
hooks `beforeJsonApiSerialize` and `afterJsonApiSerialize`.
399+
###### `options.data`
400+
The raw body data prior to deserialization from creates and updates. This can be
401+
manipulated prior to deserialization using `beforeJsonApiDeserialize`
402+
403+
###### `options.result`
404+
The deserialized raw body data. This is used when saving
405+
models as part of a create or update operation. You can manipulate this prior to
406+
the save occuring in `afterJsonApiDeserialize`
407+
408+
## Serialization/Deserialization Hooks
409+
For occasions when you don't want to fully implement (de)serialization for a model manually but
410+
you need to manipulate the serialization/deserialization process, you can use the
411+
hooks `beforeJsonApiSerialize`, `afterJsonApiSerialize`, `beforeJsonApiDeserialize` and `afterJsonApiDeserialize`.
412+
413+
### beforeJsonApiDeserialize
414+
In order to modify the deserialization process on a model by model basis, you can
415+
define a `Model.beforeJsonApiDeserialize` function as shown below. The function
416+
will be called with an options object and a callback which must be called with either
417+
an error as the first argument or the modified options object as the second
418+
parameter.
419+
420+
**Examples of things you might want to use this feature for**
421+
- modifying `options.data.data.attributes` prior to their being deserialized into model properties that
422+
will be saved
423+
- modifying `options.data.data.relationships` prior to their being used to save relationship linkages
424+
425+
#### code example
426+
```js
427+
module.exports = function (MyModel) {
428+
MyModel.beforeJsonApiDeserialize = function (options, callback) {
429+
// either return an error
430+
var err = new Error('Unwilling to deserialize record');
431+
err.status = 500;
432+
callback(err)
433+
434+
// or return modified data
435+
options.data.data.attributes.title = 'modified title';
436+
437+
// returned options.data will be deserialized by either the default deserialization process
438+
// or by a custom deserialize function if one is present on the model.
439+
callback(null, options);
440+
}
441+
}
442+
```
443+
444+
### afterJsonApiDeserialize
445+
This function will be called with an options object and a callback which must be called with either
446+
an error as the first argument or the modified options object as the second parameter.
447+
448+
**Examples of things you might want to use this feature for**
449+
- modifying `options.result` after their having being deserialized from `options.data.data.attributes`
450+
- modifying `options.data.data.relationships` prior to their being used to save relationship linkages
451+
452+
#### code example
453+
```js
454+
module.exports = function (MyModel) {
455+
MyModel.afterJsonApiDeserialize = function (options, callback) {
456+
// either return an error
457+
var err = new Error('something went wrong!');
458+
err.status = 500;
459+
callback(err)
460+
461+
// or return modified data prior to model being saved with options.result
462+
options.result.title = 'modified title';
463+
464+
callback(null, options);
465+
}
466+
}
467+
```
468+
469+
##### function parameters
470+
- `options` All config options set for the deserialization process. See the "the options object"
471+
section above for info on what options properties are available for modification.
472+
- `callback` Callback to call with error or options object.
374473

375474
### beforeJsonApiSerialize
376475
In order to modify the serialization process on a model by model basis, you can

lib/deserialize.js

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,37 @@ module.exports = function (app, options) {
2525
return regex.test(ctx.method.name);
2626
});
2727

28+
/**
29+
* Handle include relationship requests (aka sideloading)
30+
*/
31+
if (RelUtils.isRequestingIncludes(ctx)) {
32+
ctx.res.set({'Content-Type': 'application/vnd.api+json'});
33+
34+
ctx.req.isSideloadingRelationships = true;
35+
36+
if (RelUtils.isLoopbackInclude(ctx)) {
37+
return next();
38+
}
39+
40+
if (!RelUtils.shouldIncludeRelationships(ctx.req.method)) {
41+
return next(RelUtils.getInvalidIncludesError());
42+
}
43+
44+
var include = RelUtils.getIncludesArray(ctx.req.query);
45+
include = include.length === 1 ? include[0] : include;
46+
47+
ctx.args = ctx.args || {};
48+
ctx.args.filter = ctx.args.filter || {};
49+
ctx.args.filter.include = include;
50+
}
51+
2852
if (utils.shouldApplyJsonApi(ctx, options) || matches.length > 0) {
2953
// set the JSON API Content-Type response header
3054
ctx.res.set({'Content-Type': 'application/vnd.api+json'});
3155

56+
options.model = utils.getModelFromContext(ctx, app);
57+
options.method = ctx.method.name;
58+
3259
/**
3360
* Check the incoming payload data to ensure it is valid according to the
3461
* JSON API spec.
@@ -44,40 +71,24 @@ module.exports = function (app, options) {
4471
return next(new Error(errors));
4572
}
4673

47-
serverRelations = utils.getRelationsFromContext(ctx, app);
48-
49-
// transform the payload
50-
ctx.args.data = deserializer(data, serverRelations);
51-
52-
//TODO: Rewrite to normal search model by type and FK
53-
}
74+
options.data = data;
5475

55-
/**
56-
* Handle include relationship requests (aka sideloading)
57-
*/
58-
if (RelUtils.isRequestingIncludes(ctx)) {
59-
ctx.res.set({'Content-Type': 'application/vnd.api+json'});
60-
61-
ctx.req.isSideloadingRelationships = true;
62-
63-
if (RelUtils.isLoopbackInclude(ctx)) {
64-
return next();
65-
}
76+
serverRelations = utils.getRelationsFromContext(ctx, app);
6677

67-
if (!RelUtils.shouldIncludeRelationships(ctx.req.method)) {
68-
return next(RelUtils.getInvalidIncludesError());
69-
}
78+
options.relationships = serverRelations;
7079

71-
var include = RelUtils.getIncludesArray(ctx.req.query);
72-
include = include.length === 1 ? include[0] : include;
80+
// transform the payload
81+
deserializer(options, function (err, deserializerOptions) {
82+
if (err) return next(err);
7383

74-
ctx.args = ctx.args || {};
75-
ctx.args.filter = ctx.args.filter || {};
76-
ctx.args.filter.include = include;
84+
options.data = deserializerOptions.data;
85+
ctx.args.data = deserializerOptions.result;
7786

87+
next();
88+
});
89+
} else {
90+
next();
7891
}
79-
80-
next();
8192
});
8293
};
8394

lib/deserializer.js

Lines changed: 33 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
var _ = require('lodash');
2-
var inflection = require('inflection');
2+
3+
function defaultBeforeDeserialize (options, cb) {
4+
cb(null, options);
5+
}
6+
7+
function defaultDeserialize (options, cb) {
8+
options.result = options.data.data.attributes || {};
9+
cb(null, options);
10+
}
11+
12+
function defaultAfterDeserialize (options, cb) {
13+
cb(null, options);
14+
}
315

416
/**
517
* Deserializes the requests data.
@@ -9,70 +21,28 @@ var inflection = require('inflection');
921
* @param {Object} serverRelations
1022
* @return {Object}
1123
*/
12-
module.exports = function deserializer (data, serverRelations) {
13-
var clientRelations = data.data.relationships;
14-
var result = data.data.attributes || {};
24+
module.exports = function deserializer (options, cb) {
25+
var model = options.model;
1526

16-
if (_.isPlainObject(clientRelations)) {
17-
_.each(clientRelations, function (relation, name) {
18-
handleClientRelations(data, result, serverRelations[name], relation);
19-
});
20-
}
27+
var beforeDeserialize = (typeof model.beforeJsonApiDeserialize === 'function') ?
28+
model.beforeJsonApiDeserialize : defaultBeforeDeserialize;
2129

22-
return result;
30+
var deserialize = (typeof model.jsonApiDeserialize === 'function') ?
31+
model.jsonApiDeserialize : defaultDeserialize;
2332

24-
};
33+
var afterDeserialize = (typeof model.afterJsonApiDeserialize === 'function') ?
34+
model.afterJsonApiDeserialize : defaultAfterDeserialize;
2535

26-
/**
27-
* Modifies the result for each client relation.
28-
* @private
29-
* @memberOf {Deserializer}
30-
* @param {Object} data
31-
* @param {Object} result The result object to modify.
32-
* @param {Object} serverRelation
33-
* @param {Object} clientRelationf
34-
* @return {undefined}
35-
*/
36-
function handleClientRelations (data, result, serverRelation, clientRelation) {
37-
var relationType = null;
38-
var relationValue = null;
39-
var foreignKeySuffix = 'Id';
40-
var foreignKey = serverRelation.foreignKey;
41-
42-
if (_.isPlainObject(serverRelation)) {
43-
if (_.isArray(clientRelation.data)) {
44-
if (_.isEmpty(clientRelation.data)) {
45-
relationValue = [];
46-
} else {
47-
relationType = _.result(_.find(clientRelation.data, 'type'), 'type');
48-
relationValue = _.map(clientRelation.data, function (val) {
49-
return val.id;
50-
});
51-
}
52-
53-
// pluralize
54-
foreignKeySuffix += 's';
55-
} else {
56-
if (clientRelation.data) {
57-
relationType = clientRelation.data.type;
58-
relationValue = clientRelation.data.id;
59-
}
60-
}
61-
62-
if (!relationType) {
63-
relationType = serverRelation.model;
64-
} else {
65-
relationType = inflection.singularize(relationType);
66-
if (relationType !== serverRelation.model) {
67-
return;
68-
}
69-
}
36+
var deserializeOptions = _.cloneDeep(options);
7037

71-
if (!foreignKey) {
72-
relationType = inflection.camelize(relationType, true);
73-
foreignKey = relationType + foreignKeySuffix;
74-
}
75-
76-
result[foreignKey] = relationValue;
77-
}
78-
}
38+
beforeDeserialize(deserializeOptions, function (err, deserializeOptions) {
39+
if (err) return cb(err);
40+
deserialize(deserializeOptions, function (err, deserializeOptions) {
41+
if (err) return cb(err);
42+
afterDeserialize(deserializeOptions, function (err, deserializeOptions) {
43+
if (err) return cb(err);
44+
return cb(null, deserializeOptions);
45+
});
46+
});
47+
});
48+
};

lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ module.exports = function (app, options) {
2828
debug('Started');
2929
options.debug = debug;
3030
headers(app, options);
31-
relationships(app, options);
3231
removeRemoteMethods(app, options);
3332
patch(app, options);
3433
serialize(app, options);
3534
deserialize(app, options);
35+
relationships(app, options);
3636
create(app, options);
3737
update(app, options);
3838
errors(app, options);

lib/relationships.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ module.exports = function (app, options) {
1111
var id, data, model;
1212

1313
remotes.before('**', function (ctx, next) {
14-
1514
if (utils.shouldNotApplyJsonApi(ctx, options)) {
1615
return next();
1716
};
18-
1917
id = ctx.req.params.id;
20-
data = ctx.args.data;
18+
data = options.data;
2119
model = utils.getModelFromContext(ctx, app);
2220
relationships(id, data, model);
2321

@@ -26,14 +24,15 @@ module.exports = function (app, options) {
2624

2725
// for create
2826
remotes.after('**', function (ctx, next) {
29-
3027
if (utils.shouldNotApplyJsonApi(ctx, options)) {
3128
return next();
3229
};
3330

34-
if (ctx.result) {
35-
id = ctx.result.id;
36-
data = ctx.req.body;
31+
if (ctx.method.name !== 'create') return next();
32+
33+
if (ctx.result && ctx.result.data) {
34+
id = ctx.result.data.id;
35+
data = options.data;
3736
model = utils.getModelFromContext(ctx, app);
3837
relationships(id, data, model);
3938
};
@@ -52,6 +51,7 @@ function relationships (id, data, model) {
5251
_.each(data.data.relationships, function (relationship, name) {
5352

5453
var serverRelation = model.relations[name];
54+
if (!serverRelation) return;
5555
var type = serverRelation.type;
5656
var modelTo = serverRelation.modelTo;
5757

0 commit comments

Comments
 (0)