Skip to content

Commit 75fb2f7

Browse files
committed
Implement belongsTo, fix bugs, add tests
1 parent 6b4b8eb commit 75fb2f7

File tree

3 files changed

+187
-14
lines changed

3 files changed

+187
-14
lines changed

patch.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,31 @@ function fixHttpMethod(fn, name) {
1515

1616
module.exports = function (app, options) {
1717
app.models().forEach(function(ctor) {
18-
// ctor.belongsToRemoting = function(relationName, relation, define) {
19-
// var modelName = relation.modelTo && relation.modelTo.modelName;
20-
// modelName = modelName || 'PersistedModel';
21-
// var fn = this.prototype[relationName];
22-
// var pathName = (relation.options.http && relation.options.http.path) || relationName;
23-
// define('__get__' + relationName, {
24-
// isStatic: false,
25-
// http: {verb: 'get', path: '/' + pathName},
26-
// accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
27-
// accessType: 'READ',
28-
// description: 'Fetches belongsTo relation ' + relationName + '.',
29-
// returns: {arg: relationName, type: modelName, root: true}
30-
// }, fn);
31-
// };
18+
ctor.belongsToRemoting = function(relationName, relation, define) {
19+
var modelName = relation.modelTo && relation.modelTo.modelName;
20+
modelName = modelName || 'PersistedModel';
21+
var fn = this.prototype[relationName];
22+
var pathName = (relation.options.http && relation.options.http.path) || relationName;
23+
define('__get__' + relationName, {
24+
isStatic: false,
25+
http: {verb: 'get', path: '/' + pathName},
26+
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
27+
accessType: 'READ',
28+
description: 'Fetches belongsTo relation ' + relationName + '.',
29+
returns: {arg: relationName, type: modelName, root: true}
30+
}, fn);
31+
32+
var findBelongsToRelationshipsFunc = function (cb) {
33+
this['__get__' + pathName](cb);
34+
}
35+
define('__findRelationships__' + relationName, {
36+
isStatic: false,
37+
http: {verb: 'get', path: '/relationships/' + pathName},
38+
description: 'Find relations for ' + relationName + '.',
39+
accessType: 'READ',
40+
returns: {arg: 'result', type: relation.modelTo.modelName, root: true}
41+
}, findBelongsToRelationshipsFunc);
42+
};
3243

3344
ctor.hasOneRemoting = function(relationName, relation, define) {
3445
var pathName = (relation.options.http && relation.options.http.path) || relationName;

serialize.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ function serialize(name, data, options) {
1010
//TODO: submit issue to serializer library to get this
1111
//cleaned up
1212
var dataIsNull = !data;
13+
14+
//yuck. null === 'object' so need to also check for not null
15+
//also need to exclude arrays
16+
if (!Array.isArray(data) && !!data && typeof data === 'object') {
17+
if (Object.keys(data).length === 0) {
18+
dataIsNull = true;
19+
}
20+
}
21+
1322
if (dataIsNull) {
1423
data = []
1524
}
@@ -111,6 +120,7 @@ module.exports = function (app, options) {
111120
if (ctx.req.method === 'HEAD') return next();
112121

113122
var data = clone(ctx.result)
123+
114124
var modelName = modelNameFromContext(ctx)
115125

116126
//HACK: specifically when data is null and GET :model/:id
@@ -167,8 +177,14 @@ module.exports = function (app, options) {
167177
serializeOptions.topLevelLinks.related = serializeOptions.topLevelLinks.self.replace('/relationships/', '/');
168178
}
169179

180+
//serialize the data into json api format.
170181
ctx.result = serialize(type, data, serializeOptions);
171182

183+
//once again detect that we are dealing with a relationships
184+
//url. this time post serialization.
185+
//Clean up data here by deleting resource level attributes
186+
//and links. Handle collection and single resource.
187+
//TODO: create an isRelationshipRequest helper
172188
if (serializeOptions.topLevelLinks.self.match(/\/relationships\//)) {
173189
if (ctx.result.data) {
174190
if (Array.isArray(ctx.result.data)) {

test/belongsTo.test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
var Person;
9+
var ds;
10+
11+
describe('loopback json api hasOne relationships', function() {
12+
beforeEach(function() {
13+
app = loopback();
14+
app.set('legacyExplorer', false);
15+
ds = loopback.createDataSource('memory');
16+
Post = ds.createModel('post', {
17+
id: {type: Number, id: true},
18+
title: String,
19+
content: String
20+
});
21+
app.model(Post);
22+
23+
Comment = ds.createModel('comment', {
24+
id: {type: Number, id: true},
25+
postId: Number,
26+
title: String,
27+
comment: String
28+
});
29+
app.model(Comment);
30+
Comment.settings.plural = 'comments';
31+
Comment.belongsTo(Post, {as: 'post', foreignKey: 'postId'});
32+
33+
app.use(loopback.rest());
34+
JSONAPIComponent(app);
35+
});
36+
37+
describe('Comments without a post', function (done) {
38+
beforeEach(function (done) {
39+
Comment.create({
40+
title: 'my comment',
41+
comment: 'my post comment'
42+
}, done);
43+
});
44+
45+
describe('GET /comments/1/post', function () {
46+
it('should return status code 200 OK', function (done) {
47+
request(app).get('/comments/1/post')
48+
.expect(200)
49+
.end(done);
50+
});
51+
52+
it('should return `null` keyed by `data`', function (done) {
53+
request(app).get('/comments/1/post')
54+
.end(function (err, res) {
55+
expect(res.body).to.be.an('object');
56+
expect(res.body.links).to.be.an('object');
57+
expect(res.body.links.self).to.match(/comments\/1\/post/);
58+
expect(res.body.data).to.equal(null);
59+
done();
60+
});
61+
});
62+
});
63+
64+
describe('GET /comments/1/relationships/post', function () {
65+
it('should return status code 200 OK', function (done) {
66+
request(app).get('/comments/1/relationships/post')
67+
.expect(200)
68+
.end(done);
69+
});
70+
71+
it('should return `null` keyed by `data`', function (done) {
72+
request(app).get('/comments/1/relationships/post')
73+
.end(function (err, res) {
74+
expect(res.body).to.be.an('object');
75+
expect(res.body.links).to.be.an('object');
76+
expect(res.body.links.self).to.match(/comments\/1\/relationships\/post/);
77+
expect(res.body.links.related).to.match(/comments\/1\/post/);
78+
expect(res.body.data).to.equal(null);
79+
done();
80+
});
81+
});
82+
});
83+
});
84+
85+
describe('Comment with an post', function (done) {
86+
beforeEach(function (done) {
87+
Comment.create({
88+
title: 'my comment',
89+
comment: 'my post comment'
90+
}, function (err, comment) {
91+
comment.post.create({
92+
title: 'My post',
93+
content: 'My post content'
94+
}, done);
95+
});
96+
});
97+
98+
describe('GET /comments/1/post', function () {
99+
it('should return status code 200 OK', function (done) {
100+
request(app).get('/comments/1/post')
101+
.expect(200)
102+
.end(done);
103+
});
104+
105+
it('should display a single resource object keyed by `data`', function (done) {
106+
request(app).get('/comments/1/post')
107+
.end(function (err, res) {
108+
expect(res.body).to.be.an('object');
109+
expect(res.body.links).to.be.an('object');
110+
expect(res.body.links.self).to.match(/comments\/1\/post/);
111+
expect(res.body.data.attributes).to.deep.equal({
112+
title: 'My post',
113+
content: 'My post content'
114+
});
115+
expect(res.body.data.type).to.equal('posts');
116+
expect(res.body.data.id).to.equal('1');
117+
expect(res.body.data.links.self).to.match(/^http.*\/posts\/1$/);
118+
done();
119+
});
120+
});
121+
});
122+
123+
describe('GET /comments/1/relationships/post', function () {
124+
it('should return status code 200 OK', function (done) {
125+
request(app).get('/comments/1/relationships/post')
126+
.expect(200)
127+
.end(done);
128+
});
129+
130+
it('should display a single resource object keyed by `data`', function (done) {
131+
request(app).get('/comments/1/relationships/post')
132+
.end(function (err, res) {
133+
expect(res.body).to.be.an('object');
134+
expect(res.body.links).to.be.an('object');
135+
expect(res.body.links.self).to.match(/comments\/1\/relationships\/post/);
136+
expect(res.body.links.related).to.match(/comments\/1\/post/);
137+
expect(res.body.data).to.deep.equal({
138+
type: 'posts',
139+
id: '1'
140+
});
141+
done();
142+
});
143+
});
144+
});
145+
});
146+
});

0 commit comments

Comments
 (0)