From ef456502c25f1cc541ecb7d0c8cd11ec2b8827d7 Mon Sep 17 00:00:00 2001 From: Krishna Raman Date: Thu, 25 Sep 2014 14:06:06 -0700 Subject: [PATCH 1/3] Move remote connector from loopback --- .editorconfig | 13 ++ .eslintrc | 134 ++++++++++++++++++ .gitignore | 17 +++ .jshintignore | 1 + .jshintrc | 21 +++ .npmignore | 16 +++ Gruntfile.js | 50 +++++++ index.js | 1 + lib/remote-connector.js | 90 ++++++++++++ package.json | 41 ++++++ test/remote-connector.test.js | 72 ++++++++++ test/util/describe.js | 19 +++ test/util/it.js | 19 +++ test/util/model-tests.js | 249 ++++++++++++++++++++++++++++++++++ 14 files changed, 743 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .jshintignore create mode 100644 .jshintrc create mode 100644 .npmignore create mode 100644 Gruntfile.js create mode 100644 index.js create mode 100644 lib/remote-connector.js create mode 100644 package.json create mode 100644 test/remote-connector.test.js create mode 100644 test/util/describe.js create mode 100644 test/util/it.js create mode 100644 test/util/model-tests.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3ee22e5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..962ec7c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,134 @@ +{ + "env": { + "browser": false, + "node": true, + "amd": true + }, + + "rules": { + "no-alert": 2, + "no-array-constructor": 2, + "no-bitwise": 2, + "no-caller": 2, + "no-catch-shadow": 2, + "no-comma-dangle": 2, + "no-cond-assign": 2, + "no-console": 1, + "no-constant-condition": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 1, + "no-dupe-keys": 2, + "no-else-return": 2, + "no-empty": 2, + "no-empty-class": 2, + "no-empty-label": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 1, + "no-extra-semi": 2, + "no-extra-strict": 2, + "no-fallthrough": 2, + "no-floating-decimal": 1, + "no-func-assign": 2, + "no-global-strict": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-lonely-if": 2, + "no-loop-func": 2, + "no-mixed-requires": [2, false], + "no-multi-str": 2, + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 1, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 1, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 0, + "no-plusplus": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 0, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-space-before-semi": 2, + "no-sparse-arrays": 2, + "no-sync": 0, + "no-ternary": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undefined": 1, + "no-undef-init": 2, + "no-underscore-dangle": 0, + "no-unreachable": 2, + "no-unused-expressions": 2, + "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], + "no-use-before-define": 2, + "no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], + "no-with": 2, + "no-wrap-func": 2, + "no-mixed-spaces-and-tabs": [2, false], + + "block-scoped-var": 1, + "brace-style": [2, "1tbs"], + "camelcase": 2, + "complexity": [0, 11], + "consistent-return": 2, + "consistent-this": [2, "that"], + "curly": [2, "all"], + "default-case": 1, + "dot-notation": 2, + "eol-last": 2, + "eqeqeq": 2, + "func-names": 0, + "func-style": [2, "declaration"], + "guard-for-in": 1, + "max-depth": [0, 4], + "max-len": [1, 80, 4], + "max-nested-callbacks": [1, 2], + "max-params": [0, 3], + "max-statements": [0, 10], + "handle-callback-err": 1, + "new-cap": 2, + "new-parens": 2, + "one-var": 0, + "quote-props": 1, + "quotes": [2, "single"], + "radix": 2, + "semi": 2, + "sort-vars": 1, + "space-after-keywords": [2, "always"], + "space-in-brackets": [1, "never"], + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-word-ops": 1, + "strict": 0, + "use-isnan": 2, + "valid-jsdoc": 2, + "valid-typeof": 2, + "wrap-iife": 1, + "wrap-regex": 1, + "yoda": [2, "never"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57befb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.idea +.project +*.sublime-* +.DS_Store +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.swp +*.swo +node_modules +coverage +*.tgz +*.xml +*._* diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..17430da --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "eqeqeq": true, + "eqnull": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "nonew": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "trailing": true, + "sub": true, + "maxlen": 80 +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..7ec7473 --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +.idea +.project +*.sublime-* +.DS_Store +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.swp +*.swo +node_modules +coverage +*.tgz +*.xml diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..5697bf3 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,50 @@ +/*global module:false*/ +module.exports = function(grunt) { + // Project configuration. + grunt.initConfig({ + // Metadata. + pkg: grunt.file.readJSON('package.json'), + banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + + '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + + '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + + ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', + // Task configuration. + jshint: { + options: { + jshintrc: true + }, + gruntfile: { + src: 'Gruntfile.js' + }, + lib_test: { + src: ['lib/**/*.js', 'test/**/*.js'] + } + }, + mochaTest: { + 'unit': { + src: 'test/*.js', + options: { + reporter: 'dot' + } + }, + 'unit-xml': { + src: 'test/*.js', + options: { + reporter: 'xunit', + captureFile: 'xunit.xml' + } + } + } + }); + + // These plugins provide necessary tasks. + grunt.loadNpmTasks('grunt-mocha-test'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + + // Default task. + grunt.registerTask('default', ['test']); + + grunt.registerTask('test', [ + process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit']); +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..191d30b --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/remote-connector'); \ No newline at end of file diff --git a/lib/remote-connector.js b/lib/remote-connector.js new file mode 100644 index 0000000..26beb4a --- /dev/null +++ b/lib/remote-connector.js @@ -0,0 +1,90 @@ +/** + * Dependencies. + */ + +var assert = require('assert'); +var remoting = require('strong-remoting'); +var DataAccessObject = require('loopback-datasource-juggler/lib/dao'); + +/** + * Export the RemoteConnector class. + */ + +module.exports = RemoteConnector; + +/** + * Create an instance of the connector with the given `settings`. + */ + +function RemoteConnector(settings) { + assert(typeof settings === 'object', 'cannot initiaze RemoteConnector without a settings object'); + this.client = settings.client; + this.adapter = settings.adapter || 'rest'; + this.protocol = settings.protocol || 'http' + this.root = settings.root || ''; + this.host = settings.host || 'localhost'; + this.port = settings.port || 3000; + this.remotes = remoting.create(); + + // TODO(ritch) make sure this name works with Model.getSourceId() + this.name = 'remote-connector'; + + if(settings.url) { + this.url = settings.url; + } else { + this.url = this.protocol + '://' + this.host + ':' + this.port + this.root; + } + + // handle mixins in the define() method + var DAO = this.DataAccessObject = function() {}; +} + +RemoteConnector.prototype.connect = function() { + this.remotes.connect(this.url, this.adapter); +} + +RemoteConnector.initialize = function(dataSource, callback) { + var connector = dataSource.connector = new RemoteConnector(dataSource.settings); + connector.connect(); + callback(); +} + +RemoteConnector.prototype.define = function(definition) { + var Model = definition.model; + var remotes = this.remotes; + var SharedClass; + + assert(Model.sharedClass, 'cannot attach ' + Model.modelName + + ' to a remote connector without a Model.sharedClass'); + + remotes.addClass(Model.sharedClass); + + Model + .sharedClass + .methods() + .forEach(function(remoteMethod) { + // TODO(ritch) more elegant way of ignoring a nested shared class + if(remoteMethod.name !== 'Change' + && remoteMethod.name !== 'Checkpoint') { + createProxyMethod(Model, remotes, remoteMethod); + } + }); +} + +function createProxyMethod(Model, remotes, remoteMethod) { + var scope = remoteMethod.isStatic ? Model : Model.prototype; + var original = scope[remoteMethod.name]; + + scope[remoteMethod.name] = function remoteMethodProxy() { + var args = Array.prototype.slice.call(arguments); + var lastArgIsFunc = typeof args[args.length - 1] === 'function'; + var callback; + if(lastArgIsFunc) { + callback = args.pop(); + } + + remotes.invoke(remoteMethod.stringName, args, callback); + } +} + +function noop() {} diff --git a/package.json b/package.json new file mode 100644 index 0000000..938b665 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "loopback-connector-remote", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "grunt test" + }, + "repository": { + "type": "git", + "url": "https://github.com/kraman/loopback-connector-remotekr.git" + }, + "author": "Krishna Raman ", + "license": "StrongLoop", + "bugs": { + "url": "https://github.com/kraman/loopback-connector-remotekr/issues" + }, + "homepage": "https://github.com/kraman/loopback-connector-remotekr", + "dependencies": { + "loopback-datasource-juggler": "^2.8.0", + "strong-remoting": "^2.1.0" + }, + "devDependencies": { + "assert": "^1.1.2", + "async": "^0.9.0", + "grunt": "~0.4.5", + "grunt-browserify": "~3.0.1", + "grunt-cli": "^0.1.13", + "grunt-contrib-jshint": "~0.10.0", + "grunt-karma": "~0.9.0", + "grunt-mocha-test": "^0.11.0", + "karma": "~0.12.23", + "karma-junit-reporter": "^0.2.2", + "karma-mocha": "^0.1.9", + "loopback": "^2.2.0", + "loopback-datasource-juggler": "^2.9.0", + "mocha": "~1.21.4", + "strong-task-emitter": "0.0.5", + "supertest": "~0.13.0" + } +} diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js new file mode 100644 index 0000000..6c95b46 --- /dev/null +++ b/test/remote-connector.test.js @@ -0,0 +1,72 @@ +var loopback = require('loopback'); +var defineModelTestsWithDataSource = require('./util/model-tests'); +var assert = require('assert'); + +describe('RemoteConnector', function() { + var remoteApp; + var remote; + + defineModelTestsWithDataSource({ + beforeEach: function(done) { + var test = this; + remoteApp = loopback(); + remoteApp.use(loopback.rest()); + remoteApp.listen(0, function() { + test.dataSource = loopback.createDataSource({ + host: remoteApp.get('host'), + port: remoteApp.get('port'), + connector: loopback.Remote + }); + done(); + }); + }, + onDefine: function(Model) { + var RemoteModel = Model.extend(Model.modelName); + RemoteModel.attachTo(loopback.createDataSource({ + connector: loopback.Memory + })); + remoteApp.model(RemoteModel); + } + }); + + beforeEach(function(done) { + var test = this; + remoteApp = this.remoteApp = loopback(); + remoteApp.use(loopback.rest()); + var ServerModel = this.ServerModel = loopback.PersistedModel.extend('TestModel'); + + remoteApp.model(ServerModel); + + remoteApp.listen(0, function() { + test.remote = loopback.createDataSource({ + host: remoteApp.get('host'), + port: remoteApp.get('port'), + connector: loopback.Remote + }); + done(); + }); + }); + + it('should support the save method', function (done) { + var calledServerCreate = false; + var RemoteModel = loopback.PersistedModel.extend('TestModel'); + RemoteModel.attachTo(this.remote); + + var ServerModel = this.ServerModel; + + ServerModel.create = function(data, cb) { + calledServerCreate = true; + data.id = 1; + cb(null, data); + } + + ServerModel.setupRemoting(); + + var m = new RemoteModel({foo: 'bar'}); + m.save(function(err, inst) { + assert(inst instanceof RemoteModel); + assert(calledServerCreate); + done(); + }); + }); +}); diff --git a/test/util/describe.js b/test/util/describe.js new file mode 100644 index 0000000..ff34ec2 --- /dev/null +++ b/test/util/describe.js @@ -0,0 +1,19 @@ +var loopback = require('loopback'); + +module.exports = describe; + +describe.onServer = function describeOnServer(name, fn) { + if (loopback.isServer) { + describe(name, fn); + } else { + describe.skip(name, fn); + } +}; + +describe.inBrowser = function describeInBrowser(name, fn) { + if (loopback.isBrowser) { + describe(name, fn); + } else { + describe.skip(name, fn); + } +}; diff --git a/test/util/it.js b/test/util/it.js new file mode 100644 index 0000000..a2f23b7 --- /dev/null +++ b/test/util/it.js @@ -0,0 +1,19 @@ +var loopback = require('loopback'); + +module.exports = it; + +it.onServer = function itOnServer(name, fn) { + if (loopback.isServer) { + it(name, fn); + } else { + it.skip(name, fn); + } +}; + +it.inBrowser = function itInBrowser(name, fn) { + if (loopback.isBrowser) { + it(name, fn); + } else { + it.skip(name, fn); + } +}; diff --git a/test/util/model-tests.js b/test/util/model-tests.js new file mode 100644 index 0000000..16b1e31 --- /dev/null +++ b/test/util/model-tests.js @@ -0,0 +1,249 @@ +var assert = require('assert'); +var async = require('async'); +var describe = require('./describe'); +var loopback = require('loopback'); +var ACL = loopback.ACL; +var Change = loopback.Change; +var PersistedModel = loopback.PersistedModel; +var RemoteObjects = require('strong-remoting'); +var TaskEmitter = require('strong-task-emitter'); + +module.exports = function defineModelTestsWithDataSource(options) { + +describe('Model Tests', function() { + + var User, dataSource; + + if(options.beforeEach) { + beforeEach(options.beforeEach); + } + + beforeEach(function() { + var test = this; + + // setup a model / datasource + dataSource = this.dataSource || loopback.createDataSource(options.dataSource); + + var extend = PersistedModel.extend; + + // create model hook + PersistedModel.extend = function() { + var extendedModel = extend.apply(PersistedModel, arguments); + + if(options.onDefine) { + options.onDefine.call(test, extendedModel); + } + + return extendedModel; + } + + User = PersistedModel.extend('user', { + 'first': String, + 'last': String, + 'age': Number, + 'password': String, + 'gender': String, + 'domain': String, + 'email': String + }, { + trackChanges: true + }); + + // enable destroy all for testing + User.destroyAll.shared = true; + User.attachTo(dataSource); + }); + + describe('Model.validatesPresenceOf(properties...)', function() { + it("Require a model to include a property to be considered valid", function() { + User.validatesPresenceOf('first', 'last', 'age'); + var joe = new User({first: 'joe'}); + assert(joe.isValid() === false, 'model should not validate'); + assert(joe.errors.last, 'should have a missing last error'); + assert(joe.errors.age, 'should have a missing age error'); + }); + }); + + describe('Model.validatesLengthOf(property, options)', function() { + it("Require a property length to be within a specified range", function() { + User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}}); + var joe = new User({password: '1234'}); + assert(joe.isValid() === false, 'model should not be valid'); + assert(joe.errors.password, 'should have password error'); + }); + }); + + describe('Model.validatesInclusionOf(property, options)', function() { + it("Require a value for `property` to be in the specified array", function() { + User.validatesInclusionOf('gender', {in: ['male', 'female']}); + var foo = new User({gender: 'bar'}); + assert(foo.isValid() === false, 'model should not be valid'); + assert(foo.errors.gender, 'should have gender error'); + }); + }); + + describe('Model.validatesExclusionOf(property, options)', function() { + it("Require a value for `property` to not exist in the specified array", function() { + User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']}); + var foo = new User({domain: 'www'}); + var bar = new User({domain: 'billing'}); + var bat = new User({domain: 'admin'}); + assert(foo.isValid() === false); + assert(bar.isValid() === false); + assert(bat.isValid() === false); + assert(foo.errors.domain, 'model should have a domain error'); + assert(bat.errors.domain, 'model should have a domain error'); + assert(bat.errors.domain, 'model should have a domain error'); + }); + }); + + describe('Model.validatesNumericalityOf(property, options)', function() { + it("Require a value for `property` to be a specific type of `Number`", function() { + User.validatesNumericalityOf('age', {int: true}); + var joe = new User({age: 10.2}); + assert(joe.isValid() === false); + var bob = new User({age: 0}); + assert(bob.isValid() === true); + assert(joe.errors.age, 'model should have an age error'); + }); + }); + + describe('myModel.isValid()', function() { + it("Validate the model instance", function() { + User.validatesNumericalityOf('age', {int: true}); + var user = new User({first: 'joe', age: 'flarg'}) + var valid = user.isValid(); + assert(valid === false); + assert(user.errors.age, 'model should have age error'); + }); + + it('Asynchronously validate the model', function(done) { + User.validatesNumericalityOf('age', {int: true}); + var user = new User({first: 'joe', age: 'flarg'}); + user.isValid(function (valid) { + assert(valid === false); + assert(user.errors.age, 'model should have age error'); + done(); + }); + }); + }); + + describe('Model.create([data], [callback])', function() { + it("Create an instance of Model with given data and save to the attached data source", function(done) { + User.create({first: 'Joe', last: 'Bob'}, function(err, user) { + assert(user instanceof User); + done(); + }); + }); + }); + + describe('model.save([options], [callback])', function() { + it("Save an instance of a Model to the attached data source", function(done) { + var joe = new User({first: 'Joe', last: 'Bob'}); + joe.save(function(err, user) { + assert(user.id); + assert(!err); + assert(!user.errors); + done(); + }); + }); + }); + + describe('model.updateAttributes(data, [callback])', function() { + it("Save specified attributes to the attached data source", function(done) { + User.create({first: 'joe', age: 100}, function (err, user) { + assert(!err); + assert.equal(user.first, 'joe'); + + user.updateAttributes({ + first: 'updatedFirst', + last: 'updatedLast' + }, function (err, updatedUser) { + assert(!err); + assert.equal(updatedUser.first, 'updatedFirst'); + assert.equal(updatedUser.last, 'updatedLast'); + assert.equal(updatedUser.age, 100); + done(); + }); + }); + }); + }); + + describe('Model.upsert(data, callback)', function() { + it("Update when record with id=data.id found, insert otherwise", function(done) { + User.upsert({first: 'joe', id: 7}, function (err, user) { + assert(!err); + assert.equal(user.first, 'joe'); + + User.upsert({first: 'bob', id: 7}, function (err, updatedUser) { + assert(!err); + assert.equal(updatedUser.first, 'bob'); + done(); + }); + }); + }); + }); + + describe('model.destroy([callback])', function() { + it("Remove a model from the attached data source", function(done) { + User.create({first: 'joe', last: 'bob'}, function (err, user) { + User.findById(user.id, function (err, foundUser) { + assert.equal(user.id, foundUser.id); + foundUser.destroy(function () { + User.findById(user.id, function (err, notFound) { + assert.equal(notFound, null); + done(); + }); + }); + }); + }); + }); + }); + + describe('Model.deleteById(id, [callback])', function () { + it("Delete a model instance from the attached data source", function (done) { + User.create({first: 'joe', last: 'bob'}, function (err, user) { + User.deleteById(user.id, function (err) { + User.findById(user.id, function (err, notFound) { + assert.equal(notFound, null); + done(); + }); + }); + }); + }); + }); + + describe('Model.findById(id, callback)', function() { + it("Find an instance by id", function(done) { + User.create({first: 'michael', last: 'jordan', id: 23}, function () { + User.findById(23, function (err, user) { + assert.equal(user.id, 23); + assert.equal(user.first, 'michael'); + assert.equal(user.last, 'jordan'); + done(); + }); + }); + }); + }); + + describe('Model.count([query], callback)', function() { + it("Query count of Model instances in data source", function(done) { + (new TaskEmitter()) + .task(User, 'create', {first: 'jill', age: 100}) + .task(User, 'create', {first: 'bob', age: 200}) + .task(User, 'create', {first: 'jan'}) + .task(User, 'create', {first: 'sam'}) + .task(User, 'create', {first: 'suzy'}) + .on('done', function () { + User.count({age: {gt: 99}}, function (err, count) { + assert.equal(count, 2); + done(); + }); + }); + }); + }); + +}); + + +} From 8e4bf0d8b8d9eafb5f57190960a78ebbaccbe462 Mon Sep 17 00:00:00 2001 From: Krishna Raman Date: Thu, 25 Sep 2014 14:06:54 -0700 Subject: [PATCH 2/3] Fix relation access via remote connector --- lib/relations.js | 217 ++++++++++++++++++++++++++++++++++++++++ lib/remote-connector.js | 60 ++++++----- 2 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 lib/relations.js diff --git a/lib/relations.js b/lib/relations.js new file mode 100644 index 0000000..dbd67e2 --- /dev/null +++ b/lib/relations.js @@ -0,0 +1,217 @@ +/*! + * Dependencies + */ +var relation = require('loopback-datasource-juggler/lib/relation-definition'); +var RelationDefinition = relation.RelationDefinition; + +module.exports = RelationMixin; + +/** + * RelationMixin class. Use to define relationships between models. + * + * @class RelationMixin + */ +function RelationMixin() { +} + +/** + * Define a "one to many" relationship by specifying the model name + * + * Examples: + * ``` + * User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'}); + * ``` + * + * ``` + * Book.hasMany(Chapter); + * ``` + * Or, equivalently: + * ``` + * Book.hasMany('chapters', {model: Chapter}); + * ``` + * + * Query and create related models: + * + * ```js + * Book.create(function(err, book) { + * + * // Create a chapter instance ready to be saved in the data source. + * var chapter = book.chapters.build({name: 'Chapter 1'}); + * + * // Save the new chapter + * chapter.save(); + * + * // you can also call the Chapter.create method with the `chapters` property which will build a chapter + * // instance and save the it in the data source. + * book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) { + * // this callback is optional + * }); + * + * // Query chapters for the book + * book.chapters(function(err, chapters) { // all chapters with bookId = book.id + * console.log(chapters); + * }); + * + * book.chapters({where: {name: 'test'}, function(err, chapters) { + * // All chapters with bookId = book.id and name = 'test' + * console.log(chapters); + * }); + * }); + *``` + * @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship. + * @options {Object} parameters Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object + */ +RelationMixin.hasMany = function hasMany(modelTo, params) { + var def = RelationDefinition.hasMany(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); +}; + +/** + * Declare "belongsTo" relation that sets up a one-to-one connection with another model, such that each + * instance of the declaring model "belongs to" one instance of the other model. + * + * For example, if an application includes users and posts, and each post can be written by exactly one user. + * The following code specifies that `Post` has a reference called `author` to the `User` model via the `userId` property of `Post` + * as the foreign key. + * ``` + * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); + * ``` + * You can then access the author in one of the following styles. + * Get the User object for the post author asynchronously: + * ``` + * post.author(callback); + * ``` + * Get the User object for the post author synchronously: + * ``` + * post.author(); + * Set the author to be the given user: + * ``` + * post.author(user) + * ``` + * Examples: + * + * Suppose the model Post has a *belongsTo* relationship with User (the author of the post). You could declare it this way: + * ```js + * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); + * ``` + * + * When a post is loaded, you can load the related author with: + * ```js + * post.author(function(err, user) { + * // the user variable is your user object + * }); + * ``` + * + * The related object is cached, so if later you try to get again the author, no additional request will be made. + * But there is an optional boolean parameter in first position that set whether or not you want to reload the cache: + * ```js + * post.author(true, function(err, user) { + * // The user is reloaded, even if it was already cached. + * }); + * ``` + * This optional parameter default value is false, so the related object will be loaded from cache if available. + * + * @param {Class|String} modelTo Model object (or String name of model) to which you are creating the relationship. + * @options {Object} params Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} foreignKey Name of foreign key property. + * + */ +RelationMixin.belongsTo = function(modelTo, params) { + var def = RelationDefinition.belongsTo(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); +}; + +/** + * A hasAndBelongsToMany relation creates a direct many-to-many connection with another model, with no intervening model. + * For example, if your application includes users and groups, with each group having many users and each user appearing + * in many groups, you could declare the models this way: + * ``` + * User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'}); + * ``` + * Then, to get the groups to which the user belongs: + * ``` + * user.groups(callback); + * ``` + * Create a new group and connect it with the user: + * ``` + * user.groups.create(data, callback); + * ``` + * Connect an existing group with the user: + * ``` + * user.groups.add(group, callback); + * ``` + * Remove the user from the group: + * ``` + * user.groups.remove(group, callback); + * ``` + * + * @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship. + * the relation + * @options {Object} params Configuration parameters; see below. + * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} foreignKey Property name of foreign key field. + * @property {Object} model Model object + */ +RelationMixin.hasAndBelongsToMany = + function hasAndBelongsToMany(modelTo, params) { + var def = RelationDefinition.hasAndBelongsToMany(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); + }; + +RelationMixin.hasOne = function hasOne(modelTo, params) { + var def = RelationDefinition.hasOne(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); +}; + +RelationMixin.referencesMany = function referencesMany(modelTo, params) { + var def = RelationDefinition.referencesMany(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); +}; + +RelationMixin.embedsOne = function embedsOne(modelTo, params) { + var def = RelationDefinition.embedsOne(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); +}; + +RelationMixin.embedsMany = function embedsMany(modelTo, params) { + var def = RelationDefinition.embedsMany(this, modelTo, params); + this.dataSource.adapter.resolve(this); + defineRelationProperty(this, def); +}; + +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); + }; + scope.count = function() { + return that['__count__' + def.name].apply(that, arguments); + }; + scope.create = function() { + return that['__create__' + def.name].apply(that, arguments); + }; + scope.deleteById = destroyById = function() { + return that['__destroyById__' + def.name].apply(that, arguments); + }; + scope.exists = function() { + return that['__exists__' + def.name].apply(that, arguments); + }; + scope.findById = function() { + return that['__findById__' + def.name].apply(that, arguments); + }; + return scope; + } + }); +} \ No newline at end of file diff --git a/lib/remote-connector.js b/lib/remote-connector.js index 26beb4a..e2508d3 100644 --- a/lib/remote-connector.js +++ b/lib/remote-connector.js @@ -4,7 +4,8 @@ var assert = require('assert'); var remoting = require('strong-remoting'); -var DataAccessObject = require('loopback-datasource-juggler/lib/dao'); +var jutil = require('loopback-datasource-juggler/lib/jutil'); +var RelationMixin = require('./relations'); /** * Export the RemoteConnector class. @@ -17,7 +18,9 @@ module.exports = RemoteConnector; */ function RemoteConnector(settings) { - assert(typeof settings === 'object', 'cannot initiaze RemoteConnector without a settings object'); + assert(typeof settings === + 'object', + 'cannot initiaze RemoteConnector without a settings object'); this.client = settings.client; this.adapter = settings.adapter || 'rest'; this.protocol = settings.protocol || 'http' @@ -25,18 +28,17 @@ function RemoteConnector(settings) { this.host = settings.host || 'localhost'; this.port = settings.port || 3000; this.remotes = remoting.create(); - - // TODO(ritch) make sure this name works with Model.getSourceId() this.name = 'remote-connector'; - if(settings.url) { + if (settings.url) { this.url = settings.url; } else { this.url = this.protocol + '://' + this.host + ':' + this.port + this.root; } // handle mixins in the define() method - var DAO = this.DataAccessObject = function() {}; + var DAO = this.DataAccessObject = function() { + }; } RemoteConnector.prototype.connect = function() { @@ -44,47 +46,55 @@ RemoteConnector.prototype.connect = function() { } RemoteConnector.initialize = function(dataSource, callback) { - var connector = dataSource.connector = new RemoteConnector(dataSource.settings); + var connector = dataSource.connector = + new RemoteConnector(dataSource.settings); connector.connect(); - callback(); + setImmediate(callback); } RemoteConnector.prototype.define = function(definition) { var Model = definition.model; var remotes = this.remotes; - var SharedClass; - assert(Model.sharedClass, 'cannot attach ' + Model.modelName - + ' to a remote connector without a Model.sharedClass'); + assert(Model.sharedClass, + 'cannot attach ' + + Model.modelName + + ' to a remote connector without a Model.sharedClass'); + jutil.mixin(Model, RelationMixin); remotes.addClass(Model.sharedClass); +} - Model - .sharedClass - .methods() - .forEach(function(remoteMethod) { - // TODO(ritch) more elegant way of ignoring a nested shared class - if(remoteMethod.name !== 'Change' - && remoteMethod.name !== 'Checkpoint') { - createProxyMethod(Model, remotes, remoteMethod); - } - }); +RemoteConnector.prototype.resolve = function(Model) { + var remotes = this.remotes; + + Model.sharedClass.methods().forEach(function(remoteMethod) { + if (remoteMethod.name !== 'Change' && remoteMethod.name !== 'Checkpoint') { + createProxyMethod(Model, remotes, remoteMethod); + } + }); } function createProxyMethod(Model, remotes, remoteMethod) { var scope = remoteMethod.isStatic ? Model : Model.prototype; var original = scope[remoteMethod.name]; - + scope[remoteMethod.name] = function remoteMethodProxy() { var args = Array.prototype.slice.call(arguments); var lastArgIsFunc = typeof args[args.length - 1] === 'function'; var callback; - if(lastArgIsFunc) { + if (lastArgIsFunc) { callback = args.pop(); } - remotes.invoke(remoteMethod.stringName, args, callback); + if (remoteMethod.isStatic) { + return remotes.invoke(remoteMethod.stringName, args, callback); + } + + var ctorArgs = [this.id]; + return remotes.invoke(remoteMethod.stringName, ctorArgs, args, callback); } } -function noop() {} +function noop() { +} From bb4eb26fb66ec1749d3242a668afe03e654a068e Mon Sep 17 00:00:00 2001 From: Krishna Raman Date: Fri, 26 Sep 2014 15:08:52 -0700 Subject: [PATCH 3/3] v1.0.0 --- CHANGES.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..1fb0e4f --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,13 @@ +2014-09-26, Version 1.0.0 +========================= + + * Fix strong-remoting dependency version (Krishna Raman) + + * Fix relation access via remote connector (Krishna Raman) + + * Fix formatting (Krishna Raman) + + * Move remote connector from loopback (Krishna Raman) + + * init: Initial commit (Krishna Raman) + diff --git a/package.json b/package.json index 1005ac9..a15c6c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-connector-remote", - "version": "1.0.0-beta2", + "version": "1.0.0", "description": "Remote REST API connector for Loopback", "main": "index.js", "keywords": [