Skip to content

Commit ddc0f86

Browse files
committed
Merge pull request #88 from digitalsadhu/add/support_custom_serialization
Add support for custom serialization
2 parents 2542a35 + 8a740fd commit ddc0f86

File tree

4 files changed

+401
-26
lines changed

4 files changed

+401
-26
lines changed

README.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,216 @@ be output as attributes you can specify a whitelist of attributes for each type.
220220
#### note
221221
The attributes arrays are keyed by type not by model name. Type is the term used by json api to describe the resource type in question and while not required by json api it is usually plural. In `loopback-component-jsonapi` it is whatever the models `plural` is set to in `model.json`. So in our example above we defined: `"posts": ["title", "content"]` as the resource type for the `post` model is `posts`
222222

223+
## Custom Serialization
224+
For occasions where you need greater control over the serialization process, you can implement a custom serialization function for each model as needed. This function will be used instead of the regular serialization process.
225+
226+
#### example
227+
```js
228+
module.exports = function (MyModel) {
229+
MyModel.jsonApiSerialize = function (options, callback) {
230+
// either return an error
231+
var err = new Error('Unable to serialize record');
232+
err.status = 500;
233+
cb(err)
234+
235+
// or return serialized records
236+
if (Array.isArray(options.records)) {
237+
// serialize an array of records
238+
} else {
239+
// serialize a single record
240+
}
241+
cb(null, options);
242+
}
243+
}
244+
```
245+
246+
##### function parameters
247+
248+
- `options` All config options set for the serialization process. See below.
249+
- `callback` Callback to call with error or serialized records
250+
251+
###### `options.type`
252+
Resource type. Originally calculated from a models plural. Is used in the default
253+
serialization process to set the type property for each model in a jsonapi response.
254+
- eg. `posts`
255+
256+
###### `options.method`
257+
The method that was called to get the data for the current request. This is not
258+
used in the serialization process but is provided for custom hook and serialization
259+
context.
260+
- Eg. `create`, `updateAttributes`
261+
262+
###### `options.primaryKeyField`
263+
The name of the property that is the primary key for the model. This is usually just
264+
`id` unless defined differently in a model.json file.
265+
266+
###### `options.requestedIncludes`
267+
The relationships that the user has requested be side loaded with the request.
268+
For example, for the request `GET /api/posts?include=comments` options.requestedIncludes
269+
would be `'comments'`.
270+
- Type: `string` or `array`
271+
- eg: `'comments'` or `['posts', 'comments']`
272+
273+
###### `options.host`
274+
The host part of the url including any port information.
275+
- eg. `http://localhost:3000`
276+
277+
###### `options.restApiRoot`
278+
The api prefix used before resource information. Can be used in conjunction with
279+
`options.host` and `options.type` to build up the full url for a resource.
280+
- eg. `/api`
281+
282+
###### `options.topLevelLinks`
283+
JSON API links object used at the top level of the JSON API response structure.
284+
- eg. `{links: {self: 'http://localhost:3000/api/posts'}}`
285+
286+
###### `options.dataLinks`
287+
links object used to generate links for individual resource items. The structure is
288+
and object with JSON API link keys such as `self` or `related` that are defined as
289+
a function that will be called for each resource.
290+
291+
Eg.
292+
```js
293+
options.dataLinks: {
294+
self: function (resource) {
295+
return 'http://localhost:3000/posts/' + resource.id;
296+
}
297+
}
298+
```
299+
As shown above, each resource gets passed to the function and the result of the
300+
function is assigned to the key in the final JSON API response.
301+
302+
###### `options.relationships`
303+
This contains all the relationship definitions for the model being serialized.
304+
Relationship definition objects are in the same format as in loopback's `Model.relations`
305+
definition. An object with relationship name keys, each having properties:
306+
307+
- `modelTo` loopback model object
308+
- `keyTo` name of key on to model
309+
- `modelFrom` loopback model object
310+
- `keyFrom` name of key on from model
311+
- `type` type of relationship (belongsTo, hasOne, hasMany)
312+
313+
This information is used to build relationship urls and even setup side loaded
314+
data correctly during the serialization process.
315+
316+
eg.
317+
```js
318+
options.relationships = {
319+
comments: { modelTo: ...etc },
320+
tags: { modelTo: ...etc }
321+
}
322+
```
323+
324+
###### `options.results`
325+
This is the actual data to be serialized. In `beforeJsonApiSerialize` and
326+
`jsonApiSerialize` this will be the raw data as you would ordinarily get it from
327+
loopback. In `afterJsonApiSerialize` this will be the serialized data ready for
328+
any final modifications.
329+
330+
###### `options.exclude`
331+
This is the exclude settings as defined in the `exclude` configuration option
332+
explained earlier. Use this in `beforeJsonApiSerialize` to make any model specific
333+
adjustments before serialization.
334+
335+
###### `options.attributes`
336+
This is the attributes settings as defined in the `attributes` configuration option
337+
explained earlier. Use this in `beforeJsonApiSerialize` to make any model specific
338+
adjustments before serialization.
339+
340+
## Serialization Hooks
341+
For occasions when you don't want to fully implement serialization for a model manually but
342+
you need to manipulate the serialization process, you can use the serialization
343+
hooks `beforeJsonApiSerialize` and `afterJsonApiSerialize`.
344+
345+
### beforeJsonApiSerialize
346+
In order to modify the serialization process on a model by model basis, you can
347+
define a `Model.beforeJsonApiSerialize` function as shown below. The function
348+
will be called with an options object and a callback which must be called with either
349+
an error as the first argument or the modified options object as the second
350+
parameter.
351+
352+
**Examples of things you might want to use this feature for**
353+
- modify the record(s) before serialization by modifying `options.results`
354+
- modify the resource type by modifying `options.type`
355+
- setup serialization differently depending on `options.method`
356+
- side load data (advanced)
357+
- modify the way relationships are serialized
358+
359+
#### code example
360+
```js
361+
module.exports = function (MyModel) {
362+
MyModel.beforeJsonApiSerialize = function (options, callback) {
363+
// either return an error
364+
var err = new Error('Unable to serialize record');
365+
err.status = 500;
366+
callback(err)
367+
368+
// or return modified records
369+
if (Array.isArray(options.results)) {
370+
// modify an array of records
371+
} else {
372+
// modify a single record
373+
}
374+
// returned options.records will be serialized by either the default serialization process
375+
// or by a custom serialize function (described above) if one is present on the model.
376+
callback(null, options);
377+
}
378+
}
379+
```
380+
381+
##### function parameters
382+
- `options` All config options set for the serialization process. See the "function parameters"
383+
section above for info on what options properties are available for modification.
384+
- `callback` Callback to call with error or options object.
385+
386+
#### example use case
387+
Because the `beforeJsonApiSerialize` method is passed all the options that will
388+
be used during serialization, it is possible to tweak options to affect the
389+
serialization process. One example of this is modifying the `type` option to
390+
change the resource type that will be output.
391+
392+
```js
393+
module.exports = function (MyModel) {
394+
MyModel.beforeJsonApiSerialize = function (options, callback) {
395+
options.type = 'mycustommodels';
396+
cb(null, options);
397+
}
398+
}
399+
```
400+
401+
### afterJsonApiSerialize
402+
In order to modify the serialized data on a model by model basis, you can
403+
define a `Model.afterJsonApiSerialize` function as shown below. The function
404+
will be called with an options object and a callback which must be called with either
405+
an error as the first argument or the modified options object as the second
406+
parameter.
407+
408+
#### example
409+
```js
410+
module.exports = function (MyModel) {
411+
MyModel.afterJsonApiSerialize = function (options, callback) {
412+
// either return an error
413+
var err = new Error('Unable to modify serialized record');
414+
err.status = 500;
415+
callback(err)
416+
417+
// or return modified records
418+
if (Array.isArray(options.results)) {
419+
// modify an array of serialized records
420+
} else {
421+
// modify a single serialized record
422+
}
423+
// returned options.records will be output through the api.
424+
callback(null, options);
425+
}
426+
}
427+
```
428+
429+
##### function parameters
430+
- `options` All config options set for the serialization process
431+
- `callback` Callback to call with modified serialized records
432+
223433
## Debugging
224434
You can enable debug logging by setting an environment variable:
225435
`DEBUG=loopback-component-jsonapi`

lib/serialize.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ module.exports = function (app, defaults) {
2020
modelNamePlural,
2121
relatedModelPlural,
2222
relations,
23-
res,
2423
model,
2524
requestedIncludes;
2625

@@ -84,6 +83,8 @@ module.exports = function (app, defaults) {
8483
requestedIncludes = ctx.req.remotingContext.args.filter.include;
8584
}
8685
options = {
86+
model: model,
87+
method: ctx.method.name,
8788
primaryKeyField: primaryKeyField,
8889
requestedIncludes: requestedIncludes,
8990
host: utils.hostFromContext(ctx),
@@ -111,14 +112,11 @@ module.exports = function (app, defaults) {
111112
relations = utils.getRelationsFromContext(ctx, app);
112113

113114
// Serialize our request
114-
try {
115-
res = serializer(type, data, relations, options);
116-
} catch (e) {
117-
next(e);
118-
}
119-
120-
ctx.result = res;
115+
serializer(type, data, relations, options, function (err, results) {
116+
if (err) return next(err);
121117

122-
next();
118+
ctx.result = results;
119+
next();
120+
});
123121
});
124122
};

lib/serializer.js

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,82 @@ var _ = require('lodash');
22
var RelUtils = require('./utilities/relationship-utils');
33
var utils = require('./utils');
44

5-
module.exports = function serializer (type, data, relations, options) {
5+
function defaultBeforeSerialize (options, cb) {
6+
cb(null, options);
7+
}
68

9+
function defaultSerialize (options, cb) {
710
var result = null;
811
var resultData = {};
9-
options.attributes = options.attributes || {};
10-
11-
options.isRelationshipRequest = false;
12-
13-
if (options.topLevelLinks.self.match(/\/relationships\//)) {
14-
options.topLevelLinks.related = options.topLevelLinks.self.replace('/relationships/', '/');
15-
options.isRelationshipRequest = true;
16-
}
1712

18-
if (_.isArray(data)) {
19-
result = parseCollection(type, data, relations, options);
20-
} else if (_.isPlainObject(data)) {
21-
result = parseResource(type, data, relations, options);
13+
if (_.isArray(options.results)) {
14+
result = parseCollection(options.type, options.results, options.relationships, options);
15+
} else if (_.isPlainObject(options.results)) {
16+
result = parseResource(options.type, options.results, options.relationships, options);
2217
}
2318

24-
resultData.data = result;
25-
2619
if (options.topLevelLinks) {
2720
resultData.links = makeLinks(options.topLevelLinks);
2821
}
2922

23+
resultData.data = result;
24+
3025
/**
3126
* If we're requesting to sideload relationships...
3227
*/
3328
if (options.requestedIncludes) {
34-
handleIncludes(resultData, options.requestedIncludes, relations);
29+
try {
30+
handleIncludes(resultData, options.requestedIncludes, options.relationships);
31+
} catch (err) {
32+
cb(err);
33+
}
34+
}
35+
36+
options.results = resultData;
37+
cb(null, options);
38+
}
39+
40+
function defaultAfterSerialize (options, cb) {
41+
cb(null, options);
42+
}
43+
44+
module.exports = function serializer (type, data, relations, options, cb) {
45+
options = _.clone(options);
46+
options.attributes = options.attributes || {};
47+
48+
options.isRelationshipRequest = false;
49+
50+
if (options.topLevelLinks.self.match(/\/relationships\//)) {
51+
options.topLevelLinks.related = options.topLevelLinks.self.replace('/relationships/', '/');
52+
options.isRelationshipRequest = true;
3553
}
54+
var serializeOptions;
55+
var model = options.model;
56+
57+
var beforeSerialize = (typeof model.beforeJsonApiSerialize === 'function') ?
58+
model.beforeJsonApiSerialize : defaultBeforeSerialize;
59+
60+
var serialize = (typeof model.jsonApiSerialize === 'function') ?
61+
model.jsonApiSerialize : defaultSerialize;
62+
63+
var afterSerialize = (typeof model.afterJsonApiSerialize === 'function') ?
64+
model.afterJsonApiSerialize : defaultAfterSerialize;
3665

37-
return resultData;
66+
serializeOptions = _.defaults(options, {
67+
type: type,
68+
results: data,
69+
relationships: relations
70+
});
71+
beforeSerialize(serializeOptions, function (err, serializeOptions) {
72+
if (err) return cb(err);
73+
serialize(serializeOptions, function (err, serializeOptions) {
74+
if (err) return cb(err);
75+
afterSerialize(serializeOptions, function (err, serializeOptions) {
76+
if (err) return cb(err);
77+
return cb(null, serializeOptions.results);
78+
});
79+
});
80+
});
3881
};
3982

4083
/**

0 commit comments

Comments
 (0)