Skip to content

Commit e0a4606

Browse files
committed
Merge pull request #89 from Resonance1584/fix/polymorphic_relationships
fix belongsTo and hasMany polymorphic
2 parents 5472e59 + 9f050b6 commit e0a4606

File tree

6 files changed

+228
-12
lines changed

6 files changed

+228
-12
lines changed

lib/patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function belongsToRemoting (relationName, relation, define) {
103103
},
104104
returns: {
105105
arg: 'result',
106-
type: relation.modelTo.modelName,
106+
type: modelName,
107107
root: true
108108
}
109109
}, findBelongsToRelationshipsFunc);

lib/serialize.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = function (app, defaults) {
1818
type,
1919
options,
2020
modelNamePlural,
21+
relatedModel,
2122
relatedModelPlural,
2223
relations,
2324
model,
@@ -70,11 +71,18 @@ module.exports = function (app, defaults) {
7071

7172
var relation = model.relations[relationName];
7273
if (relationName && relation) {
73-
relatedModelPlural = utils.pluralForModel(relation.modelTo);
74-
primaryKeyField = utils.primaryKeyForModel(relation.modelTo);
74+
if (relation.polymorphic && utils.relationFkOnModelFrom(relation)) {
75+
var discriminator = utils.clone(ctx.instance)[relation.polymorphic.discriminator];
76+
relatedModel = app.models[discriminator];
77+
} else {
78+
relatedModel = relation.modelTo;
79+
}
80+
relatedModelPlural = utils.pluralForModel(relatedModel);
81+
primaryKeyField = utils.primaryKeyForModel(relatedModel);
7582

7683
if (relatedModelPlural) {
7784
type = relatedModelPlural;
85+
model = relatedModel;
7886
}
7987
}
8088

@@ -83,6 +91,7 @@ module.exports = function (app, defaults) {
8391
requestedIncludes = ctx.req.remotingContext.args.filter.include;
8492
}
8593
options = {
94+
app: app,
8695
model: model,
8796
method: ctx.method.name,
8897
primaryKeyField: primaryKeyField,

lib/serializer.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function defaultSerialize (options, cb) {
2727
*/
2828
if (options.requestedIncludes) {
2929
try {
30-
handleIncludes(resultData, options.requestedIncludes, options.relationships);
30+
handleIncludes(resultData, options.requestedIncludes, options.relationships, options.app);
3131
} catch (err) {
3232
cb(err);
3333
}
@@ -167,11 +167,10 @@ function parseRelations (data, relations, options) {
167167
var collection = parts[parts.length - 1];
168168

169169
_.each(relations, function (relation, name) {
170-
171170
var fkName = relation.keyTo;
172171

173172
// If relation is belongsTo then fk is the other way around
174-
if (relation.type === 'belongsTo' || relation.type === 'referencesMany') {
173+
if (utils.relationFkOnModelFrom(relation)) {
175174
fkName = relation.keyFrom;
176175
}
177176

@@ -187,15 +186,22 @@ function parseRelations (data, relations, options) {
187186
if (fkName !== options.primaryKeyField) {
188187
delete data[fkName];
189188
}
190-
relationships = parseRelations(data, relation.modelTo.relations, options);
189+
relationships = parseRelations(data, options.model.relations, options);
191190
return false;
192191
}
193192

194193
var pk = data[options.primaryKeyField];
195194
var fk = data[fkName];
196195

197196
var fromType = utils.pluralForModel(relation.modelFrom);
198-
var toType = utils.pluralForModel(relation.modelTo);
197+
var toType = '';
198+
if (relation.polymorphic && utils.relationFkOnModelFrom(relation)) {
199+
var discriminator = relation.polymorphic.discriminator;
200+
var model = options.app.models[data[discriminator]];
201+
toType = utils.pluralForModel(model);
202+
} else {
203+
toType = utils.pluralForModel(relation.modelTo);
204+
}
199205

200206
// Relationship `links` should always be defined unless this is a
201207
// relationship request
@@ -306,7 +312,7 @@ function makeLinks (links, item) {
306312
* @throws {Error}
307313
* @return {undefined}
308314
*/
309-
function handleIncludes (resp, includes, relations) {
315+
function handleIncludes (resp, includes, relations, app) {
310316
var resources = _.isArray(resp.data) ? resp.data : [resp.data];
311317

312318
if (typeof includes === 'string') {
@@ -321,11 +327,17 @@ function handleIncludes (resp, includes, relations) {
321327
return includes.map(function (include) {
322328
var relation = relations[include];
323329
var propertyKey = relation.keyFrom;
324-
var plural = utils.pluralForModel(relation.modelTo);
330+
var plural = '';
331+
if (relation.polymorphic && utils.relationFkOnModelFrom(relation)) {
332+
var discriminator = resource.attributes[relation.polymorphic.discriminator];
333+
plural = utils.pluralForModel(app.models[discriminator]);
334+
} else {
335+
plural = utils.pluralForModel(relation.modelTo);
336+
}
325337
var embeds = [];
326338

327339
// If relation is belongsTo then pk and fk are the other way around
328-
if (relation.type === 'belongsTo' || relation.type === 'referencesMany') {
340+
if (utils.relationFkOnModelFrom(relation)) {
329341
propertyKey = relation.keyTo;
330342
}
331343

lib/utils.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ module.exports = {
1616
urlFromContext: urlFromContext,
1717
primaryKeyForModel: primaryKeyForModel,
1818
shouldNotApplyJsonApi: shouldNotApplyJsonApi,
19-
shouldApplyJsonApi: shouldApplyJsonApi
19+
shouldApplyJsonApi: shouldApplyJsonApi,
20+
relationFkOnModelFrom: relationFkOnModelFrom
2021
};
2122

2223
function primaryKeyForModel (model) {
@@ -204,3 +205,7 @@ function shouldNotApplyJsonApi (ctx, options) {
204205
}
205206
return false;
206207
}
208+
209+
function relationFkOnModelFrom (relation) {
210+
return relation.type === 'belongsTo' || relation.type === 'referencesMany';
211+
}

test/belongsToPolymorphic.test.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 ds;
7+
var Post;
8+
var File;
9+
10+
describe('loopback json api belongsTo polymorphic relationships', function () {
11+
beforeEach(function () {
12+
app = loopback();
13+
app.set('legacyExplorer', false);
14+
ds = loopback.createDataSource('memory');
15+
Post = ds.createModel('post', {
16+
id: {type: Number, id: true},
17+
title: String,
18+
content: String
19+
});
20+
Post.settings.plural = 'posts';
21+
app.model(Post);
22+
23+
File = ds.createModel('file', {
24+
id: {type: Number, id: true},
25+
fileName: String,
26+
parentId: Number,
27+
parentType: String
28+
});
29+
File.settings.plural = 'files';
30+
File.belongsTo('parent', {
31+
polymorphic: {
32+
foreignKey: 'parentId',
33+
discriminator: 'parentType'
34+
}
35+
});
36+
app.model(File);
37+
38+
app.use(loopback.rest());
39+
JSONAPIComponent(app);
40+
});
41+
42+
describe('File belonging to a Post', function () {
43+
beforeEach(function (done) {
44+
Post.create({
45+
title: 'Post One',
46+
content: 'Content'
47+
}, function (err, post) {
48+
expect(err).to.equal(null);
49+
File.create({
50+
fileName: 'blah.jpg',
51+
parentId: post.id,
52+
parentType: 'post'
53+
}, done);
54+
});
55+
});
56+
57+
it('should have a relationship to Post', function (done) {
58+
request(app).get('/files/1')
59+
.end(function (err, res) {
60+
expect(err).to.equal(null);
61+
expect(res.body).to.not.have.key('errors');
62+
expect(res.body.data.relationships.parent).to.be.an('object');
63+
done();
64+
});
65+
});
66+
67+
it('should return the Post that this file belongs to when included flag is present', function (done) {
68+
request(app).get('/files/1?include=parent')
69+
.end(function (err, res) {
70+
expect(err).to.equal(null);
71+
expect(res.body).to.not.have.key('errors');
72+
expect(res.body.included).to.be.an('array');
73+
expect(res.body.included[0].type).to.equal('posts');
74+
expect(res.body.included[0].id).to.equal('1');
75+
done();
76+
});
77+
});
78+
79+
it('should return the Post that this file belongs to when following the relationship link', function (done) {
80+
request(app).get('/files/1')
81+
.end(function (err, res) {
82+
expect(err).to.equal(null);
83+
expect(res.body).to.not.have.key('errors');
84+
request(app).get(res.body.data.relationships.parent.links.related.split('api')[1])
85+
.end(function (err, res) {
86+
expect(err).to.equal(null);
87+
expect(res.body).to.not.have.key('errors');
88+
expect(res.body.data.type).to.equal('posts');
89+
expect(res.body.data.id).to.equal('1');
90+
done();
91+
});
92+
});
93+
});
94+
});
95+
});

test/hasManyPolymorphic.test.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 ds;
7+
var Post;
8+
var File;
9+
10+
describe('loopback json api hasMany polymorphic relationships', function () {
11+
beforeEach(function () {
12+
app = loopback();
13+
app.set('legacyExplorer', false);
14+
ds = loopback.createDataSource('memory');
15+
16+
File = ds.createModel('file', {
17+
id: {type: Number, id: true},
18+
fileName: String,
19+
parentId: Number,
20+
parentType: String
21+
});
22+
File.settings.plural = 'files';
23+
app.model(File);
24+
25+
Post = ds.createModel('post', {
26+
id: {type: Number, id: true},
27+
title: String,
28+
content: String
29+
});
30+
Post.settings.plural = 'posts';
31+
Post.hasMany(File, {
32+
as: 'files',
33+
polymorphic: 'parent'
34+
});
35+
app.model(Post);
36+
37+
app.use(loopback.rest());
38+
JSONAPIComponent(app);
39+
});
40+
41+
describe('Post hasMany Files', function () {
42+
beforeEach(function (done) {
43+
Post.create({
44+
title: 'Post One',
45+
content: 'Content'
46+
}, function (err, post) {
47+
expect(err).to.equal(null);
48+
post.files.create({
49+
fileName: 'blah.jpg',
50+
parentId: post.id,
51+
parentType: 'post'
52+
}, done);
53+
});
54+
});
55+
56+
it('should have a relationship to Files', function (done) {
57+
request(app).get('/posts/1')
58+
.end(function (err, res) {
59+
expect(err).to.equal(null);
60+
expect(res.body).to.not.have.key('errors');
61+
expect(res.body.data.relationships.files).to.be.an('object');
62+
done();
63+
});
64+
});
65+
66+
it('should return the Files that belong to this Post when included flag is present', function (done) {
67+
request(app).get('/posts/1?include=files')
68+
.end(function (err, res) {
69+
expect(err).to.equal(null);
70+
expect(res.body).to.not.have.key('errors');
71+
expect(res.body.included).to.be.an('array');
72+
expect(res.body.included[0].type).to.equal('files');
73+
expect(res.body.included[0].id).to.equal('1');
74+
done();
75+
});
76+
});
77+
78+
it('should return the Files that belong to this Post when following the relationship link', function (done) {
79+
request(app).get('/posts/1')
80+
.end(function (err, res) {
81+
expect(err).to.equal(null);
82+
expect(res.body).to.not.have.key('errors');
83+
request(app).get(res.body.data.relationships.files.links.related.split('api')[1])
84+
.end(function (err, res) {
85+
expect(err).to.equal(null);
86+
expect(res.body).to.not.have.key('errors');
87+
expect(res.body.data).to.be.an('array');
88+
expect(res.body.data[0].type).to.equal('files');
89+
expect(res.body.data[0].id).to.equal('1');
90+
done();
91+
});
92+
});
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)