From ef456502c25f1cc541ecb7d0c8cd11ec2b8827d7 Mon Sep 17 00:00:00 2001 From: Krishna Raman Date: Thu, 25 Sep 2014 14:06:06 -0700 Subject: [PATCH] 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(); + }); + }); + }); + }); + +}); + + +}