Skip to content

Commit 78ce8e7

Browse files
committed
Implement "exclude" option api
1 parent 72d398d commit 78ce8e7

File tree

11 files changed

+287
-4
lines changed

11 files changed

+287
-4
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ Example:
6767
"loopback-component-jsonapi": {
6868
"restApiRoot": "/api",
6969
"enable": true,
70-
"handleErrors": true
70+
"handleErrors": true,
71+
"exclude": [
72+
{"model": "comment"},
73+
{"methods": "find"},
74+
{"model": "post", "methods": "find"},
75+
{"model": "person", "methods": ["find", "create"]}
76+
]
7177
}
7278
}
7379
```
@@ -87,6 +93,34 @@ format. Validation errors include the correct properties in order to work
8793
out of the box with ember.
8894
Default: true
8995

96+
### exclude
97+
Allows blacklisting of models and methods. (See example above)
98+
Define an array of blacklist objects. Blacklist objects can contain "model" key
99+
"methods" key or both. If just "model" is defined then all methods for the
100+
specified model will not use jsonapi. If just the "methods" key is defined then
101+
all methods specified on all models will be not use jsonapi. If a combination of
102+
"model" and "methods" keys are used then the specific combination of model and methods
103+
specified will not use jsonapi.
104+
105+
#### Please note
106+
The default component behavior currently is to only modify the output of the following
107+
methods on all models to be json api compliant:
108+
- find
109+
- create
110+
- updateAttributes
111+
- deleteById
112+
- findById
113+
- __get__.*
114+
- __findRelationships__.*
115+
116+
And the default current behavior for modifying input only applies to the following methods on
117+
all models:
118+
- create
119+
- updateAttributes
120+
121+
Type: array
122+
Default: null
123+
90124
## Debugging
91125
You can enable debug logging by setting an environment variable:
92126
DEBUG=loopback-component-jsonapi

lib/create.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var url = require('url');
2+
var utils = require('./utils');
23

34
module.exports = function (app, options) {
45
//get remote methods.
@@ -9,6 +10,10 @@ module.exports = function (app, options) {
910
//register after remote method hook on all methods
1011
remotes.after('**', function (ctx, next) {
1112

13+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
14+
return next();
15+
};
16+
1217
//in this case we are only interested in handling create operations.
1318
if (ctx.method.name === 'create') {
1419
//JSON API specifies that created resources should have the

lib/delete.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
var utils = require('./utils');
2+
13
module.exports = function (app, options) {
24
//get remote methods.
35
//set strong-remoting for more information
@@ -7,6 +9,10 @@ module.exports = function (app, options) {
79
//register after remote method hook on all methods
810
remotes.after('**', function (ctx, next) {
911

12+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
13+
return next();
14+
};
15+
1016
//in this case we are only interested in handling create operations.
1117
if (ctx.method.name === 'deleteById') {
1218
//JSON API specifies that successful

lib/deserialize.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ var deserializer = require('./deserializer');
33
var RelUtils = require('./utilities/relationship-utils');
44
var utils = require('./utils');
55

6-
module.exports = function (app) {
6+
module.exports = function (app, options) {
77
/**
88
* Register a handler to run before all remote methods so that we can
99
* transform JSON API structured JSON payload into something loopback
1010
* can work with.
1111
*/
1212
app.remotes().before('**', function (ctx, next) {
1313
var data, serverRelations, errors;
14+
15+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
16+
return next();
17+
};
18+
1419
var regexs = [
1520
/^create$/,
1621
/^updateAttributes$/

lib/headers.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
'use strict';
22
var is = require('type-is');
33
var _ = require('lodash');
4+
var utils = require('./utils');
45

56
module.exports = function (app, options) {
67
var remotes = app.remotes();
78

89
remotes.before('**', function (ctx, next) {
10+
11+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
12+
return next();
13+
};
14+
915
// We must force `application/json` until we can override it through strong remoting
1016
if (ctx.req.accepts('application/vnd.api+json')) {
1117
ctx.req.headers.accept = 'application/json';

lib/relationships.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ module.exports = function (app, options) {
1212

1313
remotes.before('**', function (ctx, next) {
1414

15+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
16+
return next();
17+
};
18+
1519
id = ctx.req.params.id;
1620
data = ctx.args.data;
1721
model = utils.getModelFromContext(ctx, app);
@@ -23,6 +27,10 @@ module.exports = function (app, options) {
2327
// for create
2428
remotes.after('**', function (ctx, next) {
2529

30+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
31+
return next();
32+
};
33+
2634
if (ctx.result) {
2735
id = ctx.result.id;
2836
data = ctx.req.body;

lib/serialize.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ module.exports = function (app, defaults) {
2424
model,
2525
requestedIncludes;
2626

27+
if (utils.shouldNotApplyJsonApi(ctx, defaults)) {
28+
return next();
29+
};
30+
2731
var matches = regexs.filter(function (regex) {
2832
return regex.test(ctx.method.name);
2933
});

lib/update.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
var utils = require('./utils');
2+
13
module.exports = function (app, options) {
24
var remotes = app.remotes();
35

46
remotes.after('**', function (ctx, next) {
7+
8+
if (utils.shouldNotApplyJsonApi(ctx, options)) {
9+
return next();
10+
};
11+
512
if (ctx.method.name === 'updateAttributes') {
613
// remote links object from resource response
714
delete ctx.result.links;

lib/utils.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var url = require('url');
22
var inflection = require('inflection');
3+
var _ = require('lodash');
34

45
/**
56
* Public API
@@ -13,7 +14,8 @@ module.exports = {
1314
modelNameFromContext: modelNameFromContext,
1415
pluralForModel: pluralForModel,
1516
urlFromContext: urlFromContext,
16-
primaryKeyForModel: primaryKeyForModel
17+
primaryKeyForModel: primaryKeyForModel,
18+
shouldNotApplyJsonApi: shouldNotApplyJsonApi
1719
};
1820

1921
function primaryKeyForModel (model) {
@@ -164,3 +166,22 @@ function buildModelUrl (protocol, host, apiRoot, modelName, id) {
164166

165167
return result;
166168
}
169+
170+
function shouldNotApplyJsonApi (ctx, options) {
171+
//handle options.exclude
172+
if (!options.exclude) return false;
173+
174+
var modelName = ctx.method.sharedClass.name;
175+
var methodName = ctx.method.name;
176+
var model;
177+
var methods;
178+
for (var i = 0; i < options.exclude.length; i++) {
179+
model = options.exclude[i].model;
180+
methods = options.exclude[i].methods;
181+
if (model === modelName && !methods) return true;
182+
if (!model && methods === methodName) return true;
183+
if (model === modelName && methods === methodName) return true;
184+
if (model === modelName && _.contains(methods, methodName)) return true;
185+
}
186+
return false;
187+
}

test/exclude.test.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
var request = require('supertest');
2+
var loopback = require('loopback');
3+
var expect = require('chai').expect;
4+
var JSONAPIComponent = require('../');
5+
var app;
6+
var Post;
7+
var Comment;
8+
9+
describe('exclude option', function () {
10+
beforeEach(function (done) {
11+
app = loopback();
12+
app.set('legacyExplorer', false);
13+
var ds = loopback.createDataSource('memory');
14+
15+
Post = ds.createModel('post', { title: String });
16+
app.model(Post);
17+
18+
Comment = ds.createModel('comment', { comment: String });
19+
app.model(Comment);
20+
21+
app.use(loopback.rest());
22+
23+
Post.create({title: 'my post'}, function () {
24+
Comment.create({comment: 'my comment'}, done);
25+
});
26+
});
27+
28+
describe('excluding a specific model', function () {
29+
beforeEach(function () {
30+
JSONAPIComponent(app, {
31+
exclude: [
32+
{model: 'post'}
33+
]
34+
});
35+
});
36+
it('should not apply jsonapi to post model method input', function (done) {
37+
request(app).post('/posts')
38+
.send({ title: 'my post' })
39+
.set('Accept', 'application/vnd.api+json')
40+
.set('Content-Type', 'application/json')
41+
.expect(200)
42+
.end(function (err, res) {
43+
expect(err).to.equal(null);
44+
expect(res.body).to.deep.equal({ title: 'my post', id: 2 });
45+
done();
46+
});
47+
});
48+
it('should not apply jsonapi to post model output', function (done) {
49+
request(app).get('/posts')
50+
.expect(200)
51+
.end(function (err, res) {
52+
expect(err).to.equal(null);
53+
expect(res.body).to.deep.equal([ { title: 'my post', id: 1 } ]);
54+
done();
55+
});
56+
});
57+
it('should apply jsonapi to comment model method output', function (done) {
58+
request(app).get('/comments')
59+
.expect(200)
60+
.end(function (err, res) {
61+
expect(err).to.equal(null);
62+
expect(res.body.data[0]).to.have.keys('type', 'id', 'attributes', 'links');
63+
done();
64+
});
65+
});
66+
});
67+
68+
describe('excluding a specific method', function () {
69+
beforeEach(function () {
70+
JSONAPIComponent(app, {
71+
exclude: [
72+
{methods: 'find'}
73+
]
74+
});
75+
});
76+
77+
it('should not apply jsonapi to post model output for find method', function (done) {
78+
request(app).get('/posts')
79+
.expect(200)
80+
.end(function (err, res) {
81+
expect(err).to.equal(null);
82+
expect(res.body).to.deep.equal([ { title: 'my post', id: 1 } ]);
83+
done();
84+
});
85+
});
86+
87+
it('should not apply jsonapi to comment model output for find method', function (done) {
88+
request(app).get('/comments')
89+
.expect(200)
90+
.end(function (err, res) {
91+
expect(err).to.equal(null);
92+
expect(res.body).to.deep.equal([ { comment: 'my comment', id: 1 } ]);
93+
done();
94+
});
95+
});
96+
97+
it('should apply jsonapi to post model output for findById method', function (done) {
98+
request(app).get('/posts/1')
99+
.expect(200)
100+
.end(function (err, res) {
101+
expect(err).to.equal(null);
102+
expect(res.body.data).to.have.keys('type', 'id', 'attributes', 'links');
103+
done();
104+
});
105+
});
106+
107+
it('should apply jsonapi to comment model output for findById method', function (done) {
108+
request(app).get('/comments/1')
109+
.expect(200)
110+
.end(function (err, res) {
111+
expect(err).to.equal(null);
112+
expect(res.body.data).to.have.keys('type', 'id', 'attributes', 'links');
113+
done();
114+
});
115+
});
116+
});
117+
118+
describe('excluding a specific method on a specific model', function () {
119+
beforeEach(function () {
120+
JSONAPIComponent(app, {
121+
exclude: [
122+
{model: 'post', methods: 'find'}
123+
]
124+
});
125+
});
126+
127+
it('should not apply jsonapi to post model output for find method', function (done) {
128+
request(app).get('/posts')
129+
.expect(200)
130+
.end(function (err, res) {
131+
expect(err).to.equal(null);
132+
expect(res.body).to.deep.equal([ { title: 'my post', id: 1 } ]);
133+
done();
134+
});
135+
});
136+
137+
it('should apply jsonapi to post model output for findById method', function (done) {
138+
request(app).get('/posts/1')
139+
.expect(200)
140+
.end(function (err, res) {
141+
expect(err).to.equal(null);
142+
expect(res.body.data).to.have.keys('type', 'id', 'attributes', 'links');
143+
done();
144+
});
145+
});
146+
147+
it('should apply jsonapi to comment model output for find method', function (done) {
148+
request(app).get('/comments')
149+
.expect(200)
150+
.end(function (err, res) {
151+
expect(err).to.equal(null);
152+
expect(res.body.data[0]).to.have.keys('type', 'id', 'attributes', 'links');
153+
done();
154+
});
155+
});
156+
});
157+
158+
describe('excluding a specific set of methods on a specific model', function () {
159+
beforeEach(function () {
160+
JSONAPIComponent(app, {
161+
exclude: [
162+
{model: 'post', methods: ['find', 'findById']}
163+
]
164+
});
165+
});
166+
167+
it('should not apply jsonapi to post model output for find method', function (done) {
168+
request(app).get('/posts')
169+
.expect(200)
170+
.end(function (err, res) {
171+
expect(err).to.equal(null);
172+
expect(res.body).to.deep.equal([ { title: 'my post', id: 1 } ]);
173+
done();
174+
});
175+
});
176+
177+
it('should not apply jsonapi to post model output for findById method', function (done) {
178+
request(app).get('/posts/1')
179+
.expect(200)
180+
.end(function (err, res) {
181+
expect(err).to.equal(null);
182+
expect(res.body).to.deep.equal({ title: 'my post', id: 1 });
183+
done();
184+
});
185+
});
186+
});
187+
});

0 commit comments

Comments
 (0)