From a3d110b78c4388a51c4145958cb9266ad3ee9d11 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Mon, 16 Oct 2017 19:43:31 +0300 Subject: [PATCH] Preserve related models from "include" filter Before this change, when making a remote call with "include" filter (for example `findById(11, {include:['children']})`), the related models were removed from the result. This commit fixes the implementation to correctly preserve related models and also to cast them to correct model instances. --- lib/relations.js | 19 ++++- lib/remote-connector.js | 12 ++- test/remote-models.test.js | 160 ++++++++++++++++++++++++++++++++++++- 3 files changed, 185 insertions(+), 6 deletions(-) diff --git a/lib/relations.js b/lib/relations.js index 7636a9b..5827a43 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -211,10 +211,23 @@ RelationMixin.embedsMany = function embedsMany(modelTo, params) { function defineRelationProperty(modelClass, def) { Object.defineProperty(modelClass.prototype, def.name, { get: function() { - var that = this; - var scope = function() { - return that['__get__' + def.name].apply(that, arguments); + const that = this; + const scope = function() { + const cachedEntities = that.__cachedRelations && + that.__cachedRelations[def.name]; + + if (arguments.length || !cachedEntities) { + return that['__get__' + def.name].apply(that, arguments); + } + + // return the cached data + if (Array.isArray(cachedEntities)) { + return cachedEntities.map(data => new def.modelTo(data)); + } else { + return new def.modelTo(cachedEntities); + } }; + scope.count = function() { return that['__count__' + def.name].apply(that, arguments); }; diff --git a/lib/remote-connector.js b/lib/remote-connector.js index b28e081..d33c2f8 100644 --- a/lib/remote-connector.js +++ b/lib/remote-connector.js @@ -87,7 +87,17 @@ RemoteConnector.prototype.resolve = function(Model) { // setup a remoting type converter for this model remotes.defineObjectType(Model.modelName, function(data) { - return new Model(data); + const model = new Model(data); + + // process cached relations + if (model.__cachedRelations) { + for (const relation in model.__cachedRelations) { + const relatedModel = model.__cachedRelations[relation]; + model.__data[relation] = relatedModel; + } + } + + return model; }); }; diff --git a/test/remote-models.test.js b/test/remote-models.test.js index 9b7d1dc..7bc52bb 100644 --- a/test/remote-models.test.js +++ b/test/remote-models.test.js @@ -11,7 +11,8 @@ const loopback = require('loopback'); const TaskEmitter = require('strong-task-emitter'); describe('Remote model tests', function() { - let serverApp, ServerModel, clientApp, ClientModel; + let serverApp, ServerModel, ServerRelatedModel, ServerModelWithSingleChild, + clientApp, ClientModel, ClientRelatedModel, ClientModelWithSingleChild; beforeEach(function setupServer(done) { const app = serverApp = helper.createRestAppAndListen(); @@ -19,10 +20,46 @@ describe('Remote model tests', function() { ServerModel = app.registry.createModel({ name: 'TestModel', - properties: helper.userProperties, + properties: helper.getUserProperties(), + options: { + forceId: false, + relations: { + children: { + type: 'hasMany', + model: 'ChildModel', + foreignKey: 'parentId', + }, + }, + }, + }); + + ServerModelWithSingleChild = app.registry.createModel({ + name: 'TestModelWithSingleChild', + properties: helper.getUserProperties(), + options: { + forceId: false, + relations: { + child: { + type: 'hasOne', + model: 'ChildModel', + foreignKey: 'parentId', + }, + }, + }, + }); + + ServerRelatedModel = app.registry.createModel({ + name: 'ChildModel', + properties: { + note: {type: 'text'}, + parentId: {type: 'number'}, + }, options: {forceId: false}, }); + app.model(ServerModel, {dataSource: db}); + app.model(ServerRelatedModel, {dataSource: db}); + app.model(ServerModelWithSingleChild, {dataSource: db}); serverApp.locals.handler.on('listening', function() { done(); }); }); @@ -31,15 +68,56 @@ describe('Remote model tests', function() { const app = clientApp = loopback({localRegistry: true}); const remoteDs = helper.createRemoteDataSource(clientApp, serverApp); + ClientRelatedModel = app.registry.createModel({ + name: 'ChildModel', + properties: { + note: {type: 'text'}, + parentId: {type: 'number'}, + }, + options: { + strict: true, + }, + }); + ClientModel = app.registry.createModel({ name: 'TestModel', + properties: helper.getUserProperties(), + options: { + relations: { + children: { + type: 'hasMany', + model: 'ChildModel', + foreignKey: 'parentId', + }, + }, + strict: true, + }, }); + + ClientModelWithSingleChild = app.registry.createModel({ + name: 'TestModelWithSingleChild', + properties: helper.getUserProperties(), + options: { + relations: { + child: { + type: 'hasOne', + model: 'ChildModel', + foreignKey: 'parentId', + }, + }, + strict: true, + }, + }); + app.model(ClientModel, {dataSource: remoteDs}); + app.model(ClientRelatedModel, {dataSource: remoteDs}); + app.model(ClientModelWithSingleChild, {dataSource: remoteDs}); }); afterEach(function() { serverApp.locals.handler.close(); ServerModel = null; + ServerRelatedModel = null; ClientModel = null; }); @@ -160,4 +238,82 @@ describe('Remote model tests', function() { }); }); }); + + describe('Model find with include filter', function() { + let hasManyParent, hasManyChild, hasOneParent, hasOneChild; + beforeEach(givenSampleData); + + it('should return also the included requested models', function() { + const parentId = hasManyParent.id; + return ClientModel.findById(hasManyParent.id, {include: 'children'}) + .then(returnedUser => { + assert(returnedUser instanceof ClientModel); + const user = returnedUser.toJSON(); + assert.equal(user.id, parentId); + assert.equal(user.first, hasManyParent.first); + assert(Array.isArray(user.children)); + assert.equal(user.children.length, 1); + assert.deepEqual(user.children[0], hasManyChild.toJSON()); + }); + }); + + it('should return cachedRelated entity without call', function() { + const parentId = hasManyParent.id; + return ClientModel.findById(parentId, {include: 'children'}) + .then(returnedUser => { + assert(returnedUser instanceof ClientModel); + const children = returnedUser.children(); + assert.equal(returnedUser.id, parentId); + assert.equal(returnedUser.first, hasManyParent.first); + assert(Array.isArray(children)); + assert.equal(children.length, 1); + assert(children[0] instanceof ClientRelatedModel); + assert.deepEqual(children[0].toJSON(), hasManyChild.toJSON()); + }); + }); + + it('should also work for single (non array) relations', function() { + const parentId = hasOneParent.id; + return ClientModelWithSingleChild.findById(parentId, {include: 'child'}) + .then(returnedUser => { + assert(returnedUser instanceof ClientModelWithSingleChild); + const child = returnedUser.child(); + assert.equal(returnedUser.id, parentId); + assert.equal(returnedUser.first, hasOneParent.first); + assert(child instanceof ClientRelatedModel); + assert.deepEqual(child.toJSON(), hasOneChild.toJSON()); + }); + }); + + function givenSampleData() { + return ServerModel.create({first: 'eiste', last: 'kopries'}) + .then(parent => { + hasManyParent = parent; + return ServerRelatedModel.create({ + note: 'mitsos', + parentId: parent.id, + id: 11, + }); + }) + .then(child => { + hasManyChild = child; + return ServerModelWithSingleChild.create({ + first: 'mipos', + last: 'tora', + id: 12, + }); + }) + .then(parent => { + hasOneParent = parent; + return ServerRelatedModel.create({ + note: 'mitsos3', + parentId: parent.id, + id: 13, + }); + }) + .then(child => { + hasOneChild = child; + }); + } + }); });