Skip to content

Commit a36263c

Browse files
authored
Merge pull request #131 from digitalsadhu/add_foreign_key_removal_configuration
Add foreign key removal configuration
2 parents db746e3 + 407b266 commit a36263c

File tree

6 files changed

+202
-16
lines changed

6 files changed

+202
-16
lines changed

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,55 @@ be output as attributes you can specify a whitelist of attributes for each type.
270270
#### note
271271
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`
272272

273+
### foreignKeys
274+
Allows configuration of whether the component should expose foreign keys (which the jsonapi spec considers
275+
implementation details) from the attributes hash.
276+
277+
#### examples
278+
279+
Always expose foreign keys for all models
280+
```js
281+
{
282+
...
283+
foreignKeys: true,
284+
...
285+
}
286+
```
287+
288+
Never expose foreign keys for any models (default behaviour)
289+
```js
290+
{
291+
...
292+
foreignKeys: false,
293+
...
294+
}
295+
```
296+
297+
Only expose foreign keys for the commeht model
298+
```js
299+
{
300+
...
301+
foreignKeys: [
302+
{model: 'comment'}
303+
],
304+
...
305+
}
306+
```
307+
308+
Only expose foreign keys for the comment model findById method. eg. `GET /api/comments/1`
309+
```js
310+
{
311+
...
312+
foreignKeys: [
313+
{model: 'comment', method: 'findById'}
314+
],
315+
...
316+
}
317+
```
318+
319+
- Type: `boolean|array`
320+
- Default: `false`
321+
273322
## Custom Serialization
274323
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.
275324

lib/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ var debug = require('debug')('loopback-component-jsonapi')
1616
module.exports = function (app, options) {
1717
var defaultOptions = {
1818
restApiRoot: '/api',
19-
enable: true
19+
enable: true,
20+
foreignKeys: false
2021
}
2122
options = options || {}
2223
options = _.defaults(options, defaultOptions)

lib/serializer.js

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,34 @@ function parseResource (type, data, relations, options) {
103103
resource.relationships = relationships
104104
}
105105

106-
// Remove any foreign keys from this resource
107-
options.app.models().forEach(function (model) {
108-
_.each(model.relations, function (relation) {
109-
var fkModel = relation.modelTo
110-
var fkName = relation.keyTo
111-
if (utils.relationFkOnModelFrom(relation)) {
112-
fkModel = relation.modelFrom
113-
fkName = relation.keyFrom
114-
}
115-
if (fkModel === options.model && fkName !== options.primaryKeyField) {
116-
delete data[fkName]
117-
}
106+
if (options.foreignKeys !== true) {
107+
// Remove any foreign keys from this resource
108+
options.app.models().forEach(function (model) {
109+
_.each(model.relations, function (relation) {
110+
var fkModel = relation.modelTo
111+
var fkName = relation.keyTo
112+
if (utils.relationFkOnModelFrom(relation)) {
113+
fkModel = relation.modelFrom
114+
fkName = relation.keyFrom
115+
}
116+
if (fkModel === options.model && fkName !== options.primaryKeyField) {
117+
// check options and decide whether to remove foreign keys.
118+
if (options.foreignKeys !== false && Array.isArray(options.foreignKeys)) {
119+
for (var i = 0; i < options.foreignKeys.length; i++) {
120+
// if match on model
121+
if (options.foreignKeys[i].model === fkModel.sharedClass.name) {
122+
// if no method specified
123+
if (!options.foreignKeys[i].method) return
124+
// if method match
125+
if (options.foreignKeys[i].method === options.method) return
126+
}
127+
}
128+
}
129+
delete data[fkName]
130+
}
131+
})
118132
})
119-
})
133+
}
120134

121135
_.each(data, function (value, property) {
122136
if (property === options.primaryKeyField) {

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"type-is": "^1.6.9"
4040
},
4141
"devDependencies": {
42-
"babel-eslint": "^6.1.0",
4342
"chai": "^3.3.0",
4443
"coveralls": "^2.11.9",
4544
"istanbul": "^0.4.2",

test/create.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('loopback json api component create method', function () {
6161
// waiting on a fix
6262
// see https://github.com/visionmedia/superagent/issues/753
6363
query(app).post('/posts', data, options, function (err, res) {
64-
if (err) console.log(err)
64+
expect(err).to.equal(null)
6565
expect(res.headers['content-type']).to.match(/application\/vnd\.api\+json/)
6666
expect(res.statusCode).to.equal(201)
6767
expect(res.body).to.have.all.keys('data')

test/foreign-keys.test.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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('foreign key configuration', 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+
Comment.belongsTo(Post)
22+
Post.hasMany(Comment)
23+
24+
app.use(loopback.rest())
25+
26+
Post.create({title: 'my post'}, function (err, post) {
27+
if (err) throw err
28+
post.comments.create({comment: 'my comment'}, done)
29+
})
30+
})
31+
32+
describe('by default, foreign keys are not exposed through the api', function () {
33+
beforeEach(function () {
34+
JSONAPIComponent(app)
35+
})
36+
it('should remove foreign keys from model before output', function (done) {
37+
request(app).get('/comments/1')
38+
.end(function (err, res) {
39+
expect(err).to.equal(null)
40+
expect(res.body.data.attributes).to.not.include.key('postId')
41+
done()
42+
})
43+
})
44+
})
45+
46+
describe('configuring component to always expose foreign keys through the api', function () {
47+
beforeEach(function () {
48+
JSONAPIComponent(app, {
49+
foreignKeys: true
50+
})
51+
})
52+
it('should not remove foreign keys from models before output', function (done) {
53+
request(app).get('/comments/1')
54+
.end(function (err, res) {
55+
expect(err).to.equal(null)
56+
expect(res.body.data.attributes).to.include.key('postId')
57+
done()
58+
})
59+
})
60+
})
61+
62+
describe('configuring component to expose foreign keys for post model through the api', function () {
63+
beforeEach(function () {
64+
JSONAPIComponent(app, {
65+
foreignKeys: [
66+
{model: 'post'}
67+
]
68+
})
69+
})
70+
it('should not expose postId on comment model', function (done) {
71+
request(app).get('/comments/1')
72+
.end(function (err, res) {
73+
expect(err).to.equal(null)
74+
expect(res.body.data.attributes).to.not.include.key('postId')
75+
done()
76+
})
77+
})
78+
})
79+
80+
describe('configuring component to expose foreign keys for comment model through the api', function () {
81+
beforeEach(function () {
82+
JSONAPIComponent(app, {
83+
foreignKeys: [
84+
{model: 'comment'}
85+
]
86+
})
87+
})
88+
it('should expose postId on comment model', function (done) {
89+
request(app).get('/comments/1')
90+
.end(function (err, res) {
91+
expect(err).to.equal(null)
92+
expect(res.body.data.attributes).to.include.key('postId')
93+
done()
94+
})
95+
})
96+
})
97+
98+
describe('configuring component to expose foreign keys for comment model method findById through the api', function () {
99+
beforeEach(function () {
100+
JSONAPIComponent(app, {
101+
foreignKeys: [
102+
{model: 'comment', method: 'findById'}
103+
]
104+
})
105+
})
106+
it('should not expose foreign keys in find all', function (done) {
107+
request(app).get('/comments')
108+
.end(function (err, res) {
109+
expect(err).to.equal(null)
110+
expect(res.body.data[0].attributes).to.not.include.key('postId')
111+
done()
112+
})
113+
})
114+
it('should expose foreign keys in find', function (done) {
115+
request(app).get('/comments/1')
116+
.end(function (err, res) {
117+
expect(err).to.equal(null)
118+
expect(res.body.data.attributes).to.include.key('postId')
119+
done()
120+
})
121+
})
122+
})
123+
})

0 commit comments

Comments
 (0)