diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..cfd7b873 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,235 @@ +/*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. + uglify: { + options: { + banner: '<%= banner %>' + }, + dist: { + files: { + 'dist/loopback.min.js': ['dist/loopback.js'] + } + } + }, + jshint: { + options: { + jshintrc: true + }, + gruntfile: { + src: 'Gruntfile.js' + }, + lib_test: { + src: ['lib/**/*.js', 'test/**/*.js'] + } + }, + watch: { + gruntfile: { + files: '<%= jshint.gruntfile.src %>', + tasks: ['jshint:gruntfile'] + }, + lib_test: { + files: '<%= jshint.lib_test.src %>', + tasks: ['jshint:lib_test'] + } + }, + browserify: { + dist: { + files: { + 'dist/loopback.js': ['index.js'], + }, + options: { + ignore: ['nodemailer', 'passport'], + standalone: 'loopback' + } + } + }, + karma: { + unit: { + options: { + // base path, that will be used to resolve files and exclude + basePath: '', + + // frameworks to use + frameworks: ['mocha', 'browserify'], + + // list of files / patterns to load in the browser + files: [ + 'test/support.js', + 'test/model.test.js', + 'test/geo-point.test.js' + ], + + // list of files to exclude + exclude: [ + + ], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['dots'], + + // web server port + port: 9876, + + // cli runner port + runnerPort: 9100, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: 'warn', + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: [ + 'Chrome' + ], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false, + + // Browserify config (all optional) + browserify: { + // extensions: ['.coffee'], + ignore: [ + 'nodemailer', + 'passport', + 'passport-local', + 'superagent', + 'supertest' + ], + // transform: ['coffeeify'], + // debug: true, + // noParse: ['jquery'], + watch: true, + }, + + // Add browserify to preprocessors + preprocessors: {'test/*': ['browserify']} + } + }, + e2e: { + options: { + // base path, that will be used to resolve files and exclude + basePath: '', + + // frameworks to use + frameworks: ['mocha', 'browserify'], + + // list of files / patterns to load in the browser + files: [ + 'test/e2e/remote-connector.e2e.js' + ], + + // list of files to exclude + exclude: [ + + ], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['dots'], + + // web server port + port: 9876, + + // cli runner port + runnerPort: 9100, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: 'warn', + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: [ + 'Chrome' + ], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false, + + // Browserify config (all optional) + browserify: { + // extensions: ['.coffee'], + ignore: [ + 'nodemailer', + 'passport', + 'passport-local', + 'superagent', + 'supertest' + ], + // transform: ['coffeeify'], + // debug: true, + // noParse: ['jquery'], + watch: true, + }, + + // Add browserify to preprocessors + preprocessors: {'test/e2e/*': ['browserify']} + } + } + } + + }); + + // These plugins provide necessary tasks. + grunt.loadNpmTasks('grunt-browserify'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-karma'); + + grunt.registerTask('e2e-server', function() { + var done = this.async(); + var app = require('./test/fixtures/e2e/app'); + app.listen(3000, done); + }); + + grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']); + + // Default task. + grunt.registerTask('default', ['browserify']); + +}; diff --git a/LICENSE b/LICENSE index 4808ef39..8de4196b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,10 @@ -Copyright (c) 2013 StrongLoop, Inc. +Copyright (c) 2013-2014 StrongLoop, Inc. + +loopback uses a 'dual license' model. Users may use loopback under the terms of +the MIT license, or under the StrongLoop License. The text of both is included +below. + +MIT license Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -17,3 +23,289 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +StrongLoop License + +STRONGLOOP SUBSCRIPTION AGREEMENT +PLEASE READ THIS AGREEMENT CAREFULLY BEFORE YOU AGREE TO THESE TERMS. IF YOU +ARE ACTING ON BEHALF OF AN ENTITY, THEN YOU REPRESENT THAT YOU HAVE THE +AUTHORITY TO ENTER INTO THIS AGREEMENT ON BEHALF OF THAT ENTITY. IF YOU DO NOT +AGREE TO THESE TERMS, YOU SHOULD NOT AGREE TO THE TERMS OF THIS AGREEMENT OR +INSTALL OR USE THE SOFTWARE. +This StrongLoop Subscription Agreement ("Agreement") is made by and between +StrongLoop, Inc. ("StrongLoop") with its principal place of business at 107 S. +B St, Suite 220, San Mateo, CA 94401 and the person or entity entering into this +Agreement ("Customer"). The effective date ("Effective Date") of this Agreement +is the date Customer agrees to these terms or installs or uses the Software (as +defined below). This Agreement applies to Customer's use of the Software but it +shall be superseded by any signed agreement between you and StrongLoop +concerning the Software. +1. Subscriptions and Licenses. +1.1 Subscriptions. StrongLoop offers five different subscription levels to its +customers, each as more particularly described on StrongLoop's website located +at www.strongloop.com (the "StrongLoop Site"): (1) Free; (2) Developer; (3) +Professional; (4) Gold; and (5) Platinum. The actual subscription level +applicable to Customer (the "Subscription") will be specified in the purchase +order that Customer issues to StrongLoop. This Agreement applies to Customer +regardless of the level of the Subscription selected by Customer and whether or +not Customer upgrades or downgrades its Subscription. StrongLoop hereby agrees +to provide the services as described on the StrongLoop Site for each +Subscription level during the term for which Customer has purchased the +applicable Subscription, subject to Customer paying the fees applicable to the +Subscription level purchased, if any (the "Subscription Fees"). StrongLoop may +modify the services to be provided under any Subscription upon notice to +Customer. +1.2 License Grant. Subject to the terms and conditions of this Agreement, +StrongLoop grants to Customer, during the Subscription Term (as defined in +Section 7.1 (Term and Termination) of this Agreement, a limited, non-exclusive, +non-transferable right and license, to install and use the StrongLoop Suite +software (the "Software") and the documentation made available electronically as +part of the Software (the "Documentation"), either of which may be modified +during the Term (as defined in Section 7.1 below), solely for development, +production and commercial purposes so long as Customer is using the Software to +run only one process on a given operating system at a time. This Agreement, +including but not limited to the license and restrictions contained herein, +apply to Customer regardless of whether Customer accesses the Software via +download from the StrongLoop Site or through a third-party website or service, +even if Customer acquired the Software prior to agreeing to this Agreement. +1.3 License Restrictions. Customer shall not itself, or through any parent, +subsidiary, affiliate, agent or other third party: + 1.3.1 sell, lease, license, distribute, sublicense or otherwise transfer + in whole or in part, any Software or the Documentation to a third party; + or + 1.3.2 decompile, disassemble, translate, reverse engineer or otherwise + attempt to derive source code from the Software, in whole or in part, nor + shall Customer use any mechanical, electronic or other method to trace, + decompile, disassemble, or identify the source code of the Software or + encourage others to do so, except to the limited extent, if any, that + applicable law permits such acts notwithstanding any contractual + prohibitions, provided, however, before Customer exercises any rights that + Customer believes to be entitled to based on mandatory law, Customer shall + provide StrongLoop with thirty (30) days prior written notice and provide + all reasonably requested information to allow StrongLoop to assess + Customer's claim and, at StrongLoop's sole discretion, to provide + alternatives that reduce any adverse impact on StrongLoop's intellectual + property or other rights; or + 1.3.3 allow access or permit use of the Software by any users other than + Customer's employees or authorized third-party contractors who are + providing services to Customer and agree in writing to abide by the terms + of this Agreement, provided further that Customer shall be liable for any + failure by such employees and third-party contractors to comply with the + terms of this Agreement and no usage restrictions, if any, shall be + exceeded; or + 1.3.4 create, develop, license, install, use, or deploy any third party + software or services to circumvent or provide access, permissions or + rights which violate the license keys embedded within the Software; or + 1.3.5 modify or create derivative works based upon the Software or + Documentation; or disclose the results of any benchmark test of the + Software to any third party without StrongLoop's prior written approval; + or + 1.3.6 change any proprietary rights notices which appear in the Software + or Documentation; or + 1.3.7 use the Software as part of a time sharing or service bureau + purposes or in any other resale capacity. +1.4 Third-Party Software. The Software may include individual certain software +that is owned by third parties, including individual open source software +components (the "Third-Party Software"), each of which has its own copyright and +its own applicable license conditions. Such third-party software is licensed to +Customer under the terms of the applicable third-party licenses and/or copyright +notices that can be found in the LICENSES file, the Documentation or other +materials accompanying the Software, except that Sections 5 (Warranty +Disclaimer) and 6 (Limitation of Liability) also govern Customer's use of the +third-party software. Customer agrees to comply with the terms and conditions +of the relevant third-party software licenses. +2. Support Services. StrongLoop has no obligation to provide any support for +the Software other than the support services specifically described on the +StrongLoop Site for the Subscription level procured by Customer. However, +StrongLoop has endeavored to establish a community of users of the Software who +have provided their own feedback, hints and advice regarding their experiences +in using the Software. You can find that community and user feedback on the +StrongLoop Site. The use of any information, content or other materials from, +contained in or on the StrongLoop Site are subject to the StrongLoop website +terms of use located here http://www.strongloop.com/terms-of-service. +3. Confidentiality. For purposes of this Agreement, "Confidential Information" +means any and all information or proprietary materials (in every form and media) +not generally known in the relevant trade or industry and which has been or is +hereafter disclosed or made available by StrongLoop to Customer in connection +with the transactions contemplated under this Agreement, including (i) all trade +secrets, (ii) existing or contemplated Software, services, designs, technology, +processes, technical data, engineering, techniques, methodologies and concepts +and any related information, and (iii) information relating to business plans, +sales or marketing methods and customer lists or requirements. For a period of +five (5) years from the date of disclosure of the applicable Confidential +Information, Customer shall (i) hold the Confidential Information in trust and +confidence and avoid the disclosure or release thereof to any other person or +entity by using the same degree of care as it uses to avoid unauthorized use, +disclosure, or dissemination of its own Confidential Information of a similar +nature, but not less than reasonable care, and (ii) not use the Confidential +Information for any purpose whatsoever except as expressly contemplated under +this Agreement; provided that, to the extent the Confidential Information +constitutes a trade secret under law, Customer agrees to protect such +information for so long as it qualifies as a trade secret under applicable law. +Customer shall disclose the Confidential Information only to those of its +employees and contractors having a need to know such Confidential Information +and shall take all reasonable precautions to ensure that such employees and +contractors comply with the provisions of this Section. The obligations of +Customer under this Section shall not apply to information that Customer can +demonstrate (i) was in its possession at the time of disclosure and without +restriction as to confidentiality, (ii) at the time of disclosure is generally +available to the public or after disclosure becomes generally available to the +public through no breach of agreement or other wrongful act by Customer, (iii) +has been received from a third party without restriction on disclosure and +without breach of agreement by Customer, or (iv) is independently developed by +Customer without regard to the Confidential Information. In addition, Customer +may disclose Confidential Information as required to comply with binding orders +of governmental entities that have jurisdiction over it; provided that Customer +gives StrongLoop reasonable written notice to allow StrongLoop to seek a +protective order or other appropriate remedy, discloses only such Confidential +Information as is required by the governmental entity, and uses commercially +reasonable efforts to obtain confidential treatment for any Confidential +Information disclosed. Notwithstanding the above, Customer agrees that +StrongLoop, its employees and agents shall be free to use and employ their +general skills, know-how, and expertise, and to use, disclose, and employ any +generalized ideas, concepts, know-how, methods, techniques or skills gained or +learned during the Term or thereafter. +4. Ownership. StrongLoop shall retain all intellectual property and proprietary +rights in the Software, Documentation, and related works, including but not +limited to any derivative work of the foregoing and StrongLoop's licensors shall +retain all intellectual property and proprietary rights in any Third-Party +Software that may be provided with or as a part of the Software. Customer shall +do nothing inconsistent with StrongLoop's or its licensors' title to the +Software and the intellectual property rights embodied therein, including, but +not limited to, transferring, loaning, selling, assigning, pledging, or +otherwise disposing, encumbering, or suffering a lien or encumbrance upon or +against any interest in the Software. The Software (including any Third-Party +Software) contain copyrighted material, trade secrets and other proprietary +material of StrongLoop and/or its licensors. +5. Warranty Disclaimer. THE SOFTWARE (INCLUDING ANY THIRD-PARTY SOFTWARE) AND +DOCUMENTATION MADE AVAILABLE TO CUSTOMER ARE PROVIDED "AS-IS" AND STRONGLOOP, +ON BEHALF OF ITSELF AND ITS LICENSORS, EXPRESSLY DISCLAIMS ALL WARRANTIES OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE, +PERFORMANCE, AND ACCURACY AND ANY IMPLIED WARRANTIES ARISING FROM STATUTE, +COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE. STRONGLOOP DOES +NOT WARRANT THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR +ERROR-FREE, THAT DEFECTS IN THE SOFTWARE WILL BE CORRECTED OR THAT THE SOFTWARE +WILL PROVIDE OR ENSURE ANY PARTICULAR RESULTS OR OUTCOME. NO ORAL OR WRITTEN +INFORMATION OR ADVICE GIVEN BY STRONGLOOP OR ITS AUTHORIZED REPRESENTATIVES +SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF THIS WARRANTY. +STRONGLOOP IS NOT OBLIGATED TO PROVIDE CUSTOMER WITH UPGRADES TO THE SOFTWARE, +BUT MAY ELECT TO DO SO IN ITS SOLE DISCRETION. SOME JURISDICTIONS DO NOT ALLOW +THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO +CUSTOMER.WITHOUT LIMITING THE GENERALITY OF THE FOREGOING DISCLAIMER, THE +SOFTWARE AND DOCUMENTATION ARE NOT DESIGNED, MANUFACTURED OR INTENDED FOR USE IN +THE PLANNING, CONSTRUCTION, MAINTENANCE, CONTROL, OR DIRECT OPERATION OF NUCLEAR +FACILITIES, AIRCRAFT NAVIGATION, CONTROL OR COMMUNICATION SYSTEMS, WEAPONS +SYSTEMS, OR DIRECT LIFE SUPPORT SYSTEMS. +6. Limitation of Liability. + 6.1 Exclusion of Liability. IN NO EVENT WILL STRONGLOOP OR ITS LICENSORS + BE LIABLE UNDER THIS AGREEMENT FOR ANY INDIRECT, RELIANCE, PUNITIVE, + CONSEQUENTIAL, SPECIAL, EXEMPLARY, OR INCIDENTAL DAMAGES OF ANY KIND AND + HOWEVER CAUSED (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF + BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION AND + THE LIKE), EVEN IF STRONGLOOP HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + DAMAGES. CUSTOMER BEARS FULL RESPONSIBILITY FOR USE OF THE SOFTWARE AND + THE SUBSCRIPTION AND STRONGLOOP DOES NOT GUARANTEE THAT THE USE OF THE + SOFTWARE AND SUBSCRIPTION WILL ENSURE THAT CUSTOMER'S NETWORK WILL BE + AVAILABLE, SECURE, MONITORED OR PROTECTED AGAINST ANY DOWNTIME, DENIAL OF + SERVICE ATTACKS, SECUITY BREACHES, HACKERS AND THE LIKE. IN NO EVENT WILL + STRONGLOOP'S CUMULATIVE LIABILITY FOR ANY DAMAGES, LOSSES AND CAUSES OF + ACTION (WHETHER IN CONTRACT, TORT, INCLUDING NEGLIGENCE, OR OTHERWISE) + ARISING OUT OF OR RELATED TO THIS AGREEMENT EXCEED THE GREATER OF ONE + HUNDRED DOLLARS (US$100) OR THE TOTAL SUBSCRIPTION FEES PAID BY CUSTOMER + TO STRONGLOOP IN THE TWELVE (12) MONTHS PRECEDING THE DATE THE CLAIM + ARISES. + 6.2 Limitation of Damages. IN NO EVENT WILL STRONGLOOP'S LICENSORS HAVE + ANY LIABILITY FOR ANY CLAIM ARISING IN CONNECTION WITH THIS AGREEMENT. + THE PROVISIONS OF THIS SECTION 6 ALLOCATE RISKS UNDER THIS AGREEMENT + BETWEEN CUSTOMER, STRONGLOOP AND STRONGLOOP'S SUPPLIERS. THE FOREGOING + LIMITATIONS, EXCLUSIONS AND DISCLAIMERS APPLY TO THE MAXIMUM EXTENT + PERMITTED BY APPLICABLE LAW, EVEN IF ANY REMEDY FAILS IN ITS ESSENTIAL + PURPOSE. + 6.3 Failure of Essential Purpose. THE PARTIES AGREE THAT THESE + LIMITATIONS SHALL APPLY EVEN IF THIS AGREEMENT OR ANY LIMITED REMEDY + SPECIFIED HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE. + 6.4 Allocation of Risk. The sections on limitation of liability and + disclaimer of warranties allocate the risks in the Agreement between the + parties. This allocation is an essential element of the basis of the + bargain between the parties. +7. Term and Termination. +7.1 This Agreement shall commence on the Effective Date and continue for so long +as Customer has a valid Subscription and is current on the payment of any +Subscription Fees required to be paid for that Subscription (the "Subscription +Term"). Either party may terminate this Agreement immediately upon written +notice to the other party, and the Subscription and licenses granted hereunder +automatically terminate upon the termination of this Agreement. This Agreement +will terminate immediately without notice from StrongLoop if Customer fails to +comply with or otherwise breaches any provision of this Agreement. +7.2 All Sections other than Section 1.1 (Subscriptions) and 1.2 (Licenses) shall +survive the expiration or termination of this Agreement. +8. Subscription Fees and Payments. StrongLoop, Customer agrees to pay +StrongLoop the Subscription Fees as described on the StrongLoop Site for the +Subscription purchased unless a different amount has been agreed to in a +separate agreement between Customer and StrongLoop. In addition, Customer shall +pay all sales, use, value added, withholding, excise taxes and other tax, duty, +custom and similar fees levied upon the delivery or use of the Software and the +Subscriptions described in this Agreement. Fees shall be invoiced in full upon +StrongLoop's acceptance of Customer's purchase order for the Subscription. All +invoices shall be paid in US dollars and are due upon receipt and shall be paid +within thirty (30) days. Payments shall be made without right of set-off or +chargeback. If Customer does not pay the invoices when due, StrongLoop may +charge interest at one percent (1%) per month or the highest rate permitted by +law, whichever is lower, on the unpaid balance from the original due date. If +Customer fails to pay fees in accordance with this Section, StrongLoop may +suspend fulfilling its obligations under this Agreement (including but not +limited to suspending the services under the Subscription) until payment is +received by StrongLoop. If any applicable law requires Customer to withhold +amounts from any payments to StrongLoop under this Agreement, (a) Customer shall +effect such withholding, remit such amounts to the appropriate taxing +authorities and promptly furnish StrongLoop with tax receipts evidencing the +payments of such amounts and (b) the sum payable by Customer upon which the +deduction or withholding is based shall be increased to the extent necessary to +ensure that, after such deduction or withholding, StrongLoop receives and +retains, free from liability for such deduction or withholding, a net amount +equal to the amount StrongLoop would have received and retained absent the +required deduction or withholding. +9. General. +9.1 Compliance with Laws. Customer shall abide by all local, state, federal and +international laws, rules, regulations and orders applying to Customer's use of +the Software, including, without limitation, the laws and regulations of the +United States that may restrict the export and re-export of certain commodities +and technical data of United States origin, including the Software. Customer +agrees that it will not export or re-export the Software without the appropriate +United States or foreign government licenses. +9.2 Entire Agreement. This Agreement constitutes the entire agreement between +the parties concerning the subject matter hereof. This Agreement supersedes all +prior or contemporaneous discussions, proposals and agreements between the +parties relating to the subject matter hereof. No amendment, modification or +waiver of any provision of this Agreement shall be effective unless in writing +and signed by both parties. Any additional or different terms on any purchase +orders issued by Customer to StrongLoop shall not be binding on either party, +are hereby rejected by StrongLoop and void. +9.3 Severability. If any provision of this Agreement is held to be invalid or +unenforceable, the remaining portions shall remain in full force and effect and +such provision shall be enforced to the maximum extent possible so as to effect +the intent of the parties and shall be reformed to the extent necessary to make +such provision valid and enforceable. +9.4 Waiver. No waiver of rights by either party may be implied from any actions +or failures to enforce rights under this Agreement. +9.5 Force Majeure. Neither party shall be liable to the other for any delay or +failure to perform due to causes beyond its reasonable control (excluding +payment of monies due). +9.6 No Third Party Beneficiaries. Unless otherwise specifically stated, the +terms of this Agreement are intended to be and are solely for the benefit of +StrongLoop and Customer and do not create any right in favor of any third party. +9.7 Governing Law and Jurisdiction. This Agreement shall be governed by the +laws of the State of California, without reference to the principles of +conflicts of law. The provisions of the Uniform Computerized Information +Transaction Act and United Nations Convention on Contracts for the International +Sale of Goods shall not apply to this Agreement. The parties shall attempt to +resolve any dispute related to this Agreement informally, initially through +their respective management, and then by non-binding mediation in San Francisco +County, California. Any litigation related to this Agreement shall be brought +in the state or federal courts located in San Francisco County, California, and +only in those courts and each party irrevocably waives any objections to such +venue. +9.8 Notices. All notices must be in writing and shall be effective three (3) +days after the date sent to the other party's headquarters, Attention Chief +Financial Officer. diff --git a/README.md b/README.md index 39b89125..46464037 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # LoopBack +For a quick introduction and overview, see http://loopback.io/. + ## Documentation [See the full documentation](http://docs.strongloop.com/display/DOC/LoopBack). diff --git a/docs.json b/docs.json index caa0b189..8d60a579 100644 --- a/docs.json +++ b/docs.json @@ -12,6 +12,7 @@ "lib/models/application.js", "lib/models/email.js", "lib/models/model.js", + "lib/models/data-model.js", "lib/models/role.js", "lib/models/user.js", "lib/models/change.js", diff --git a/docs/api-model-remote.md b/docs/api-model-remote.md index 9098f275..c4dfe3ca 100644 --- a/docs/api-model-remote.md +++ b/docs/api-model-remote.md @@ -179,9 +179,9 @@ User.afterRemote('**', function (ctx, user, next) { Remote hooks are provided with a Context `ctx` object which contains transport specific data (eg. for http: `req` and `res`). The `ctx` object also has a set of consistent apis across transports. -#### ctx.user +#### ctx.req.accessToken -A `Model` representing the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user. +The `accessToken` of the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user (or other principal). #### ctx.result diff --git a/docs/api-model.md b/docs/api-model.md index 6a96e45a..5ec0beb2 100644 --- a/docs/api-model.md +++ b/docs/api-model.md @@ -49,7 +49,7 @@ var oracle = loopback.createDataSource({ User.attachTo(oracle); ``` -**Note:** until a model is attached to a data source it will **not** have any **attached methods**. +NOTE: until a model is attached to a data source it will not have any attached methods. ### Properties @@ -257,7 +257,7 @@ User.findById(23, function(err, user) { Find a single instance that matches the given where expression. ```js -User.findOne({id: 23}, function(err, user) { +User.findOne({where: {id: 23}}, function(err, user) { console.info(user.id); // 23 }); ``` diff --git a/example/client-server/client.js b/example/client-server/client.js new file mode 100644 index 00000000..4e1e423c --- /dev/null +++ b/example/client-server/client.js @@ -0,0 +1,20 @@ +var loopback = require('../../'); +var client = loopback(); +var CartItem = require('./models').CartItem; +var remote = loopback.createDataSource({ + connector: loopback.Remote, + root: 'http://localhost:3000' +}); + +client.model(CartItem); +CartItem.attachTo(remote); + +// call the remote method +CartItem.sum(1, function(err, total) { + console.log('result:', err || total); +}); + +// call a built in remote method +CartItem.find(function(err, items) { + console.log(items); +}); diff --git a/example/client-server/models.js b/example/client-server/models.js new file mode 100644 index 00000000..c14485c8 --- /dev/null +++ b/example/client-server/models.js @@ -0,0 +1,35 @@ +var loopback = require('../../'); + +var CartItem = exports.CartItem = loopback.DataModel.extend('CartItem', { + tax: {type: Number, default: 0.1}, + price: Number, + item: String, + qty: {type: Number, default: 0}, + cartId: Number +}); + +CartItem.sum = function(cartId, callback) { + this.find({where: {cartId: 1}}, function(err, items) { + var total = items + .map(function(item) { + return item.total(); + }) + .reduce(function(cur, prev) { + return prev + cur; + }, 0); + + callback(null, total); + }); +} + +loopback.remoteMethod( + CartItem.sum, + { + accepts: {arg: 'cartId', type: 'number'}, + returns: {arg: 'total', type: 'number'} + } +); + +CartItem.prototype.total = function() { + return this.price * this.qty * 1 + this.tax; +} diff --git a/example/client-server/server.js b/example/client-server/server.js new file mode 100644 index 00000000..7e466a56 --- /dev/null +++ b/example/client-server/server.js @@ -0,0 +1,24 @@ +var loopback = require('../../'); +var server = module.exports = loopback(); +var CartItem = require('./models').CartItem; +var memory = loopback.createDataSource({ + connector: loopback.Memory +}); + +server.use(loopback.rest()); +server.model(CartItem); + +CartItem.attachTo(memory); + +// test data +CartItem.create([ + {item: 'red hat', qty: 6, price: 19.99, cartId: 1}, + {item: 'green shirt', qty: 1, price: 14.99, cartId: 1}, + {item: 'orange pants', qty: 58, price: 9.99, cartId: 1} +]); + +CartItem.sum(1, function(err, total) { + console.log(total); +}); + +server.listen(3000); diff --git a/index.js b/index.js index d9099a70..739fa4f5 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ var datasourceJuggler = require('loopback-datasource-juggler'); loopback.Connector = require('./lib/connectors/base-connector'); loopback.Memory = require('./lib/connectors/memory'); loopback.Mail = require('./lib/connectors/mail'); +loopback.Remote = require('./lib/connectors/remote'); /** * Types diff --git a/lib/application.js b/lib/application.js index 746a5e4f..6e71ce7c 100644 --- a/lib/application.js +++ b/lib/application.js @@ -4,8 +4,10 @@ var DataSource = require('loopback-datasource-juggler').DataSource , ModelBuilder = require('loopback-datasource-juggler').ModelBuilder + , compat = require('./compat') , assert = require('assert') , fs = require('fs') + , _ = require('underscore') , RemoteObjects = require('strong-remoting') , swagger = require('strong-remoting/ext/swagger') , stringUtils = require('underscore.string') @@ -46,8 +48,7 @@ var app = exports = module.exports = {}; /** * Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). * - * **NOTE:** Calling `app.remotes()` multiple times will only ever return a - * single set of remote objects. + * **NOTE:** Calling `app.remotes()` more than once returns only a single set of remote objects. * @returns {RemoteObjects} */ @@ -55,7 +56,13 @@ app.remotes = function () { if(this._remotes) { return this._remotes; } else { - return (this._remotes = RemoteObjects.create()); + var options = {}; + + if(this.get) { + options = this.get('remoting'); + } + + return (this._remotes = RemoteObjects.create(options)); } } @@ -85,11 +92,13 @@ app.disuse = function (route) { * }); * ``` * - * @param {String} modelName The name of the model to define - * @options {Object} config The model's configuration - * @property {String} dataSource The `DataSource` to attach the model to - * @property {Object} [options] an object containing `Model` options - * @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language) + * @param {String} modelName The name of the model to define. + * @options {Object} config The model's configuration. + * @property {String|DataSource} dataSource The `DataSource` to which to attach the model. + * @property {Object} [options] an object containing `Model` options. + * @property {ACL[]} [options.acls] an array of `ACL` definitions. + * @property {String[]} [options.hidden] **experimental** an array of properties to hide when accessed remotely. + * @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language). * @end * @returns {ModelConstructor} the model class */ @@ -97,9 +106,11 @@ app.disuse = function (route) { app.model = function (Model, config) { if(arguments.length === 1) { assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor'); - assert(Model.pluralModelName, 'Model must have a "pluralModelName" property'); - this.remotes().exports[Model.pluralModelName] = Model; + assert(Model.modelName, 'Model must have a "modelName" property'); + var remotingClassName = compat.getClassNameForRemoting(Model); + this.remotes().exports[remotingClassName] = Model; this.models().push(Model); + clearHandlerCache(this); Model.shared = true; Model.app = this; Model.emit('attached', this); @@ -122,14 +133,11 @@ app.model = function (Model, config) { } /** - * Get the models exported by the app. Only models defined using `app.model()` - * will show up in this list. + * Get the models exported by the app. Returns only models defined using `app.model()` * - * There are two ways how to access models. + * There are two ways to access models: * - * **1. A list of all models** - * - * Call `app.models()` to get a list of all models. + * 1. Call `app.models()` to get a list of all models. * * ```js * var models = app.models(); @@ -139,12 +147,11 @@ app.model = function (Model, config) { * }); * ``` * - * **2. By model name** - * + * **2. Use `app.model` to access a model by name. * `app.model` has properties for all defined models. * - * In the following example the `Product` and `CustomerReceipt` models are - * accessed using the `models` object. + * The following example illustrates accessing the `Product` and `CustomerReceipt` models + * using the `models` object. * * ```js * var loopback = require('loopback'); @@ -171,7 +178,7 @@ app.model = function (Model, config) { * var customerReceipt = app.models.customerReceipt; * ``` * - * @returns {Array} a list of model classes + * @returns {Array} Array of model classes. */ app.models = function () { @@ -193,6 +200,7 @@ app.dataSource = function (name, config) { /** * Get all remote objects. + * @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions). */ app.remoteObjects = function () { @@ -203,26 +211,17 @@ app.remoteObjects = function () { models.forEach(function (ModelCtor) { // only add shared models if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') { - result[ModelCtor.pluralModelName] = ModelCtor; + result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor; } }); return result; } -/** - * Get the apps set of remote objects. - */ - -app.remotes = function () { - return this._remotes || (this._remotes = RemoteObjects.create()); -} - /** * Enable swagger REST API documentation. * - * > Note: This method is deprecated, use the extension - * [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead. + * **Note**: This method is deprecated. Use [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead. * * **Options** * @@ -282,11 +281,16 @@ app.enableAuth = function() { var modelId = modelInstance && modelInstance.id || req.param('id'); if(Model.checkAccess) { + // Pause the request before checking access + // See https://github.com/strongloop/loopback-storage-service/issues/7 + req.pause(); Model.checkAccess( req.accessToken, modelId, method.name, function(err, allowed) { + // Emit any cached data events that fired while checking access. + req.resume(); if(err) { console.log(err); next(err); @@ -303,34 +307,37 @@ app.enableAuth = function() { next(); } }); -} + + this.isAuthEnabled = true; +}; /** * Initialize an application from an options object or a set of JSON and JavaScript files. * - * **What happens during an app _boot_?** + * This function takes an optional argument that is either a string or an object. * - * 1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory - * 2. **Models** are created from an `options.models` object or `models.json` in the current directory - * 3. Any JavaScript files in the `./models` directory are loaded with `require()`. - * 4. Any JavaScript files in the `./boot` directory are loaded with `require()`. + * If the argument is a string, then it sets the application root directory based on the string value. Then it: + * 1. Creates DataSources from the `datasources.json` file in the application root directory. + * 2. Creates Models from the `models.json` file in the application root directory. * - * **Options** - * - * - `cwd` - _optional_ - the directory to use when loading JSON and JavaScript files - * - `models` - _optional_ - an object containing `Model` definitions - * - `dataSources` - _optional_ - an object containing `DataSource` definitions + * If the argument is an object, then it looks for `model`, `dataSources`, and `appRootDir` properties of the object. + * If the object has no `appRootDir` property then it sets the current working directory as the application root directory. + * Then it: + * 1. Creates DataSources from the `options.dataSources` object. + * 2. Creates Models from the `options.models` object. * - * > **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple - * > files may result - * > in models being **undefined** due to race conditions. To avoid this when - * > using `app.boot()` - * > make sure all models are passed as part of the `models` definition. + * In both cases, the function loads JavaScript files in the `/models` and `/boot` subdirectories of the application root directory with `require()`. + * + * **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple + * files may result in models being **undefined** due to race conditions. + * To avoid this when using `app.boot()` make sure all models are passed as part of the `models` definition. + * + * Throws an error if the config object is not valid or if boot fails. * * * **Model Definitions** * - * The following is an example of an object containing two `Model` definitions: "location" and "inventory". + * The following is example JSON for two `Model` definitions: "dealership" and "location". * * ```js * { @@ -375,20 +382,13 @@ app.enableAuth = function() { * } * } * ``` - * - * **Model definition properties** - * - * - `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to - * - `options` - _optional_ - an object containing `Model` options - * - `properties` _optional_ - an object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language) - * - * **DataSource definition properties** - * - * - `connector` - **required** - the name of the [connector](#working-with-data-sources-and-connectors) + * @options {String|Object} options Boot options; If String, this is the application root directory; if object, has below properties. + * @property {String} appRootDir Directory to use when loading JSON and JavaScript files (optional). Defaults to the current directory (`process.cwd()`). + * @property {Object} models Object containing `Model` definitions (optional). + * @property {Object} dataSources Object containing `DataSource` definitions (optional). + * @end * * @header app.boot([options]) - * @throws {Error} If config is not valid - * @throws {Error} If boot fails */ app.boot = function(options) { @@ -427,15 +427,16 @@ app.boot = function(options) { process.env.npm_package_config_host || app.get('host'); - appConfig.port = - process.env.npm_config_port || - process.env.OPENSHIFT_SLS_PORT || - process.env.OPENSHIFT_NODEJS_PORT || - process.env.PORT || - appConfig.port || - process.env.npm_package_config_port || - app.get('port') || - 3000; + appConfig.port = _.find([ + process.env.npm_config_port, + process.env.OPENSHIFT_SLS_PORT, + process.env.OPENSHIFT_NODEJS_PORT, + process.env.PORT, + appConfig.port, + process.env.npm_package_config_port, + app.get('port'), + 3000 + ], _.isFinite); appConfig.restApiRoot = appConfig.restApiRoot || @@ -540,7 +541,11 @@ function dataSourcesFromConfig(config) { function modelFromConfig(name, config, app) { var ModelCtor = require('./loopback').createModel(name, config.properties, config.options); - var dataSource = app.dataSources[config.dataSource]; + var dataSource = config.dataSource; + + if(typeof dataSource === 'string') { + dataSource = app.dataSources[dataSource]; + } assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"'); @@ -635,7 +640,12 @@ function tryReadConfig(cwd, fileName) { } } -/** +function clearHandlerCache(app) { + app._handlers = undefined; +} + +/*! + * This function is now deprecated. * Install all express middleware required by LoopBack. * * It is possible to inject your own middleware by listening on one of the @@ -784,7 +794,7 @@ app.installMiddleware = function() { * This way the port param contains always the real port number, even when * listen was called with port number 0. * - * @param {Function=} cb If specified, the callback will be added as a listener + * @param {Function} cb If specified, the callback is added as a listener * for the server's "listening" event. * @returns {http.Server} A node `http.Server` with this application configured * as the request handler. diff --git a/lib/browser-express.js b/lib/browser-express.js new file mode 100644 index 00000000..386e8159 --- /dev/null +++ b/lib/browser-express.js @@ -0,0 +1,7 @@ +module.exports = browserExpress; + +function browserExpress() { + return {}; +} + +browserExpress.errorHandler = {}; diff --git a/lib/compat.js b/lib/compat.js new file mode 100644 index 00000000..9fe324f7 --- /dev/null +++ b/lib/compat.js @@ -0,0 +1,56 @@ +var assert = require('assert'); + +/** + * Compatibility layer allowing applications based on an older LoopBack version + * to work with newer versions with minimum changes involved. + * + * You should not use it unless migrating from an older version of LoopBack. + */ + +var compat = exports; + +/** + * LoopBack versions pre-1.6 use plural model names when registering shared + * classes with strong-remoting. As the result, strong-remoting use method names + * like `Users.create` for the javascript methods like `User.create`. + * This has been fixed in v1.6, LoopBack consistently uses the singular + * form now. + * + * Turn this option on to enable the old behaviour. + * + * - `app.remotes()` and `app.remoteObjects()` will be indexed using + * plural names (Users instead of User). + * + * - Remote hooks must use plural names for the class name, i.e + * `Users.create` instead of `User.create`. This is transparently + * handled by `Model.beforeRemote()` and `Model.afterRemote()`. + * + * @type {boolean} + * @deprecated Your application should not depend on the way how loopback models + * and strong-remoting are wired together. It if does, you should update + * it to use singular model names. + */ + +compat.usePluralNamesForRemoting = false; + +/** + * Get the class name to use with strong-remoting. + * @param {function} Ctor Model class (constructor), e.g. `User` + * @return {string} Singular or plural name, depending on the value + * of `compat.usePluralNamesForRemoting` + * @internal + */ + +compat.getClassNameForRemoting = function(Ctor) { + assert( + typeof(Ctor) === 'function', + 'compat.getClassNameForRemoting expects a constructor as the argument'); + + if (compat.usePluralNamesForRemoting) { + assert(Ctor.pluralModelName, + 'Model must have a "pluralModelName" property in compat mode'); + return Ctor.pluralModelName; + } + + return Ctor.modelName; +}; diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index cb260c72..c0337f46 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -5,6 +5,7 @@ var mailer = require('nodemailer') , assert = require('assert') , debug = require('debug') + , loopback = require('../loopback') , STUB = 'STUB'; /** @@ -22,8 +23,10 @@ function MailConnector(settings) { var transports = settings.transports || []; this.transportsIndex = {}; this.transports = []; - - transports.forEach(this.setupTransport.bind(this)); + + if(loopback.isServer) { + transports.forEach(this.setupTransport.bind(this)); + } } MailConnector.initialize = function(dataSource, callback) { diff --git a/lib/connectors/remote.js b/lib/connectors/remote.js new file mode 100644 index 00000000..065682f6 --- /dev/null +++ b/lib/connectors/remote.js @@ -0,0 +1,68 @@ +/** + * Dependencies. + */ + +var assert = require('assert') + , compat = require('../compat') + , _ = require('underscore'); + +/** + * 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; + + if(settings.url) { + this.url = settings.url; + } else { + this.url = this.protocol + '://' + this.host + ':' + this.port + this.root; + } + + // handle mixins here + this.DataAccessObject = function() {}; +} + +RemoteConnector.prototype.connect = function() { +} + + +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 className = compat.getClassNameForRemoting(Model); + var url = this.url; + var adapter = this.adapter; + + Model.remotes(function(err, remotes) { + var sharedClass = getSharedClass(remotes, className); + remotes.connect(url, adapter); + sharedClass + .methods() + .forEach(Model.createProxyMethod.bind(Model)); + }); +} + +function getSharedClass(remotes, className) { + return _.find(remotes.classes(), function(sharedClass) { + return sharedClass.name === className; + }); +} +function noop() {} diff --git a/lib/loopback.js b/lib/loopback.js index fbc01857..d72b95af 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -8,26 +8,40 @@ var express = require('express') , EventEmitter = require('events').EventEmitter , path = require('path') , proto = require('./application') - , utils = require('express/node_modules/connect').utils , DataSource = require('loopback-datasource-juggler').DataSource , ModelBuilder = require('loopback-datasource-juggler').ModelBuilder - , assert = require('assert') - , i8n = require('inflection'); + , i8n = require('inflection') + , merge = require('util')._extend + , assert = require('assert'); /** - * `loopback` is the main entry for LoopBack core module. It provides static + * Main entry for LoopBack core module. It provides static properties and * methods to create models and data sources. The module itself is a function * that creates loopback `app`. For example, * - * * ```js * var loopback = require('loopback'); * var app = loopback(); * ``` + * + * @class loopback + * @header loopback */ var loopback = exports = module.exports = createApplication; +/** + * True if running in a browser environment; false otherwise. + */ + +loopback.isBrowser = typeof window !== 'undefined'; + +/** + * True if running in a server environment; false otherwise. + */ + +loopback.isServer = !loopback.isBrowser; + /** * Framework version. */ @@ -40,7 +54,12 @@ loopback.version = require('../package.json').version; loopback.mime = express.mime; -/** +/*! + * Compatibility layer, intentionally left undocumented. + */ +loopback.compat = require('./compat'); + +/*! * Create an loopback application. * * @return {Function} @@ -50,7 +69,12 @@ loopback.mime = express.mime; function createApplication() { var app = express(); - utils.merge(app, proto); + merge(app, proto); + + // Create a new instance of models registry per each app instance + app.models = function() { + return proto.models.apply(this, arguments); + }; return app; } @@ -70,16 +94,20 @@ for (var key in express) { /*! * Expose additional loopback middleware * for example `loopback.configure` etc. + * + * ***only in node*** */ -fs - .readdirSync(path.join(__dirname, 'middleware')) - .filter(function (file) { - return file.match(/\.js$/); - }) - .forEach(function (m) { - loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m); - }); +if (loopback.isServer) { + fs + .readdirSync(path.join(__dirname, 'middleware')) + .filter(function (file) { + return file.match(/\.js$/); + }) + .forEach(function (m) { + loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m); + }); +} /*! * Error handler title @@ -90,11 +118,10 @@ loopback.errorHandler.title = 'Loopback'; /** * Create a data source with passing the provided options to the connector. * - * @param {String} name (optional) - * @param {Object} options - * - * - connector - an loopback connector - * - other values - see the specified `connector` docs + * @param {String} name Optional name. + * @options {Object} Data Source options + * @property {Object} connector LoopBack connector. + * @property {*} Other properties See the relevant connector documentation. */ loopback.createDataSource = function (name, options) { @@ -115,7 +142,7 @@ loopback.createDataSource = function (name, options) { /** * Create a named vanilla JavaScript class constructor with an attached set of properties and options. * - * @param {String} name - must be unique + * @param {String} name Unique name. * @param {Object} properties * @param {Object} options (optional) */ @@ -138,7 +165,7 @@ loopback.createModel = function (name, properties, options) { } catch(e) {} return model; -} +}; /** * Add a remote method to a model. @@ -154,7 +181,7 @@ loopback.remoteMethod = function (fn, options) { }); } fn.http = fn.http || {verb: 'get'}; -} +}; /** * Create a template helper. @@ -170,7 +197,7 @@ loopback.template = function (file) { var templates = this._templates || (this._templates = {}); var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8')); return ejs.compile(str); -} +}; /** * Get an in-memory data source. Use one if it already exists. @@ -192,12 +219,12 @@ loopback.memory = function (name) { } return memory; -} +}; /** * Look up a model class by name from all models created by loopback.createModel() * @param {String} modelName The model name - * @return {Model} The model class + * @returns {Model} The model class */ loopback.getModel = function(modelName) { return loopback.Model.modelBuilder.models[modelName]; @@ -207,7 +234,7 @@ loopback.getModel = function(modelName) { * Look up a model class by the base model class. The method can be used by LoopBack * to find configured models in models.json over the base model. * @param {Model} The base model class - * @return {Model} The subclass if found or the base class + * @returns {Model} The subclass if found or the base class */ loopback.getModelByType = function(modelType) { assert(typeof modelType === 'function', 'The model type must be a constructor'); @@ -224,7 +251,7 @@ loopback.getModelByType = function(modelType) { * Set the default `dataSource` for a given `type`. * @param {String} type The datasource type * @param {Object|DataSource} dataSource The data source settings or instance - * @return {DataSource} The data source instance + * @returns {DataSource} The data source instance */ loopback.setDefaultDataSourceForType = function(type, dataSource) { @@ -236,17 +263,17 @@ loopback.setDefaultDataSourceForType = function(type, dataSource) { defaultDataSources[type] = dataSource; return dataSource; -} +}; /** * Get the default `dataSource` for a given `type`. * @param {String} type The datasource type - * @return {DataSource} The data source instance + * @returns {DataSource} The data source instance */ loopback.getDefaultDataSourceForType = function(type) { return this.defaultDataSources && this.defaultDataSources[type]; -} +}; /** * Attach any model that does not have a dataSource to @@ -265,7 +292,7 @@ loopback.autoAttach = function() { loopback.autoAttachModel(ModelCtor); } }); -} +}; loopback.autoAttachModel = function(ModelCtor) { if(ModelCtor.autoAttach) { @@ -277,13 +304,14 @@ loopback.autoAttachModel = function(ModelCtor) { ModelCtor.attachTo(ds); } -} +}; -/* +/*! * Built in models / services */ loopback.Model = require('./models/model'); +loopback.DataModel = require('./models/data-model'); loopback.Email = require('./models/email'); loopback.User = require('./models/user'); loopback.Application = require('./models/application'); @@ -304,6 +332,7 @@ var dataSourceTypes = { }; loopback.Email.autoAttach = dataSourceTypes.MAIL; +loopback.DataModel.autoAttach = dataSourceTypes.DB; loopback.User.autoAttach = dataSourceTypes.DB; loopback.AccessToken.autoAttach = dataSourceTypes.DB; loopback.Role.autoAttach = dataSourceTypes.DB; diff --git a/lib/middleware/rest.js b/lib/middleware/rest.js index 080add68..9ddef750 100644 --- a/lib/middleware/rest.js +++ b/lib/middleware/rest.js @@ -3,7 +3,6 @@ */ var loopback = require('../loopback'); -var RemoteObjects = require('strong-remoting'); /** * Export the middleware. @@ -23,7 +22,7 @@ function rest() { if(req.url === '/routes') { res.send(handler.adapter.allRoutes()); } else if(req.url === '/models') { - return res.send(remotes.toJSON()); + return res.send(app.remotes().toJSON()); } else { handler(req, res, next); } diff --git a/lib/middleware/token.js b/lib/middleware/token.js index 327989c1..185a71ce 100644 --- a/lib/middleware/token.js +++ b/lib/middleware/token.js @@ -3,7 +3,6 @@ */ var loopback = require('../loopback'); -var RemoteObjects = require('strong-remoting'); var assert = require('assert'); /*! diff --git a/lib/models/access-context.js b/lib/models/access-context.js index fa3c8e72..72b24810 100644 --- a/lib/models/access-context.js +++ b/lib/models/access-context.js @@ -194,8 +194,8 @@ Principal.SCOPE = 'SCOPE'; /** * Compare if two principals are equal - * @param p The other principal - * @returns {boolean} + * Returns true if argument principal is equal to this principal. + * @param {Object} principal The other principal */ Principal.prototype.equals = function (p) { if (p instanceof Principal) { @@ -205,11 +205,11 @@ Principal.prototype.equals = function (p) { }; /** - * A request to access protected resources + * A request to access protected resources. * @param {String} model The model name * @param {String} property * @param {String} accessType The access type - * @param {String} permission The permission + * @param {String} permission The requested permission * @returns {AccessRequest} * @class */ @@ -217,10 +217,19 @@ function AccessRequest(model, property, accessType, permission) { if (!(this instanceof AccessRequest)) { return new AccessRequest(model, property, accessType); } - this.model = model || AccessContext.ALL; - this.property = property || AccessContext.ALL; - this.accessType = accessType || AccessContext.ALL; - this.permission = permission || AccessContext.DEFAULT; + if (arguments.length === 1 && typeof model === 'object') { + // The argument is an object that contains all required properties + var obj = model || {}; + this.model = obj.model || AccessContext.ALL; + this.property = obj.property || AccessContext.ALL; + this.accessType = obj.accessType || AccessContext.ALL; + this.permission = obj.permission || AccessContext.DEFAULT; + } else { + this.model = model || AccessContext.ALL; + this.property = property || AccessContext.ALL; + this.accessType = accessType || AccessContext.ALL; + this.permission = permission || AccessContext.DEFAULT; + } if(debug.enabled) { debug('---AccessRequest---'); diff --git a/lib/models/access-token.js b/lib/models/access-token.js index acf8484e..f5f66d20 100644 --- a/lib/models/access-token.js +++ b/lib/models/access-token.js @@ -17,7 +17,7 @@ var Model = require('../loopback').Model */ var properties = { - id: {type: String, generated: true, id: 1}, + id: {type: String, id: true}, ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds created: {type: Date, default: function() { return new Date(); @@ -53,7 +53,14 @@ var AccessToken = module.exports = Model.extend('AccessToken', properties, { property: 'create', permission: 'ALLOW' } - ] + ], + relations: { + user: { + type: 'belongsTo', + model: 'User', + foreignKey: 'userId' + } + } }); /** diff --git a/lib/models/acl.js b/lib/models/acl.js index 883661e7..d7f18b6f 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -137,6 +137,50 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) { return -1; } } + + // Weigh against the principal type into 4 levels + // - user level (explicitly allow/deny a given user) + // - app level (explicitly allow/deny a given app) + // - role level (role based authorization) + // - other + // user > app > role > ... + score = score * 4; + switch(rule.principalType) { + case ACL.USER: + score += 4; + break; + case ACL.APP: + score += 3; + break; + case ACL.ROLE: + score += 2; + break; + default: + score +=1; + } + + // Weigh against the roles + // everyone < authenticated/unauthenticated < related < owner < ... + score = score * 8; + if(rule.principalType === ACL.ROLE) { + switch(rule.principalId) { + case Role.OWNER: + score += 4; + break; + case Role.RELATED: + score += 3; + break; + case Role.AUTHENTICATED: + case Role.UNAUTHENTICATED: + score += 2; + break; + case Role.EVERYONE: + score += 1; + break; + default: + score += 5; + } + } score = score * 4; score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1; return score; @@ -149,10 +193,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) { * @returns {AccessRequest} result The effective ACL */ ACL.resolvePermission = function resolvePermission(acls, req) { + if(!(req instanceof AccessRequest)) { + req = new AccessRequest(req); + } // Sort by the matching score in descending order acls = acls.sort(function (rule1, rule2) { return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req); }); + if(debug.enabled) { + debug('ACLs by order: %j', acls); + } var permission = ACL.DEFAULT; var score = 0; for (var i = 0; i < acls.length; i++) { @@ -351,6 +401,9 @@ ACL.checkAccess = function (context, callback) { inRoleTasks.push(function (done) { roleModel.isInRole(acl.principalId, context, function (err, inRole) { + if(debug.enabled) { + debug('In role %j: %j', acl.principalId, inRole); + } if (!err && inRole) { effectiveACLs.push(acl); } diff --git a/lib/models/application.js b/lib/models/application.js index fe2f370c..f537559e 100644 --- a/lib/models/application.js +++ b/lib/models/application.js @@ -50,7 +50,7 @@ var PushNotificationSettingSchema = { * Data model for Application */ var ApplicationSchema = { - id: {type: String, id: true, generated: true}, + id: {type: String, id: true}, // Basic information name: {type: String, required: true}, // The name description: String, // The description @@ -90,7 +90,7 @@ var ApplicationSchema = { modified: {type: Date, default: Date} }; -/** +/*! * Application management functions */ @@ -98,12 +98,13 @@ var crypto = require('crypto'); function generateKey(hmacKey, algorithm, encoding) { hmacKey = hmacKey || 'loopback'; - algorithm = algorithm || 'sha256'; - encoding = encoding || 'base64'; + algorithm = algorithm || 'sha1'; + encoding = encoding || 'hex'; var hmac = crypto.createHmac(algorithm, hmacKey); - var buf = crypto.randomBytes(64); + var buf = crypto.randomBytes(32); hmac.update(buf); - return hmac.digest('base64'); + var key = hmac.digest(encoding); + return key; } /** @@ -121,7 +122,7 @@ var Application = loopback.createModel('Application', ApplicationSchema); Application.beforeCreate = function (next) { var app = this; app.created = app.modified = new Date(); - app.id = generateKey('id', 'sha1'); + app.id = generateKey('id', 'md5'); app.clientKey = generateKey('client'); app.javaScriptKey = generateKey('javaScript'); app.restApiKey = generateKey('restApi'); @@ -188,8 +189,7 @@ Application.resetKeys = function (appId, cb) { /** * Authenticate the application id and key. * - * `matched` will be one of - * + * `matched` parameter is one of: * - clientKey * - javaScriptKey * - restApiKey @@ -200,7 +200,7 @@ Application.resetKeys = function (appId, cb) { * @param {String} key * @callback {Function} callback * @param {Error} err - * @param {String} matched - The matching key + * @param {String} matched The matching key */ Application.authenticate = function (appId, key, cb) { this.findById(appId, function (err, app) { @@ -208,13 +208,18 @@ Application.authenticate = function (appId, key, cb) { cb && cb(err, null); return; } - var matched = null; - ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function (k) { - if (app[k] === key) { - matched = k; + var result = null; + var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey']; + for (var i = 0; i < keyNames.length; i++) { + if (app[keyNames[i]] === key) { + result = { + application: app, + keyType: keyNames[i] + }; + break; } - }); - cb && cb(null, matched); + } + cb && cb(null, result); }); }; diff --git a/lib/models/data-model.js b/lib/models/data-model.js new file mode 100644 index 00000000..c3966036 --- /dev/null +++ b/lib/models/data-model.js @@ -0,0 +1,400 @@ +/*! + * Module Dependencies. + */ +var Model = require('./model'); +var DataAccess = require('loopback-datasource-juggler/lib/dao'); + +/** + * Extends Model with basic query and CRUD support. + * + * **Change Event** + * + * Listen for model changes using the `change` event. + * + * ```js + * MyDataModel.on('changed', function(obj) { + * console.log(obj) // => the changed model + * }); + * ``` + * + * @class DataModel + * @param {Object} data + * @param {Number} data.id The default id property + */ + +var DataModel = module.exports = Model.extend('DataModel'); + +/*! + * Configure the remoting attributes for a given function + * @param {Function} fn The function + * @param {Object} options The options + * @private + */ + +function setRemoting(fn, options) { + options = options || {}; + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + fn[opt] = options[opt]; + } + } + fn.shared = true; + // allow connectors to override the function by marking as delegate + fn._delegate = true; +} + +/*! + * Throw an error telling the user that the method is not available and why. + */ + +function throwNotAttached(modelName, methodName) { + throw new Error( + 'Cannot call ' + modelName + '.'+ methodName + '().' + + ' The ' + methodName + ' method has not been setup.' + + ' The DataModel has not been correctly attached to a DataSource!' + ); +} + +/*! + * Convert null callbacks to 404 error objects. + * @param {HttpContext} ctx + * @param {Function} cb + */ + +function convertNullToNotFoundError(ctx, cb) { + if (ctx.result !== null) return cb(); + + var modelName = ctx.method.sharedClass.name; + var id = ctx.getArgByName('id'); + var msg = 'Unkown "' + modelName + '" id "' + id + '".'; + var error = new Error(msg); + error.statusCode = error.status = 404; + cb(error); +} + +/** + * Create new instance of Model class, saved in database + * + * @param data [optional] + * @param callback(err, obj) + * callback called with arguments: + * + * - err (null or Error) + * - instance (null or Model) + */ + +DataModel.create = function (data, callback) { + throwNotAttached(this.modelName, 'create'); +}; + +setRemoting(DataModel.create, { + description: 'Create a new instance of the model and persist it into the data source', + accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'post', path: '/'} +}); + +/** + * Update or insert a model instance + * @param {Object} data The model instance data + * @param {Function} [callback] The callback function + */ + +DataModel.upsert = DataModel.updateOrCreate = function upsert(data, callback) { + throwNotAttached(this.modelName, 'updateOrCreate'); +}; + +// upsert ~ remoting attributes +setRemoting(DataModel.upsert, { + description: 'Update an existing model instance or insert a new one into the data source', + accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'put', path: '/'} +}); + +/** + * Find one record, same as `all`, limited by 1 and return object, not collection, + * if not found, create using data provided as second argument + * + * @param {Object} query - search conditions: {where: {test: 'me'}}. + * @param {Object} data - object to create. + * @param {Function} cb - callback called with (err, instance) + */ + +DataModel.findOrCreate = function findOrCreate(query, data, callback) { + throwNotAttached(this.modelName, 'findOrCreate'); +}; + +/** + * Check whether a model instance exists in database + * + * @param {id} id - identifier of object (primary key value) + * @param {Function} cb - callbacl called with (err, exists: Bool) + */ + +DataModel.exists = function exists(id, cb) { + throwNotAttached(this.modelName, 'exists'); +}; + +// exists ~ remoting attributes +setRemoting(DataModel.exists, { + description: 'Check whether a model instance exists in the data source', + accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + returns: {arg: 'exists', type: 'any'}, + http: {verb: 'get', path: '/:id/exists'} +}); + +/** + * Find object by id + * + * @param {*} id - primary key value + * @param {Function} cb - callback called with (err, instance) + */ + +DataModel.findById = function find(id, cb) { + throwNotAttached(this.modelName, 'find'); +}; + +// find ~ remoting attributes +setRemoting(DataModel.findById, { + description: 'Find a model instance by id from the data source', + accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + returns: {arg: 'data', type: 'any', root: true}, + http: {verb: 'get', path: '/:id'}, + rest: {after: convertNullToNotFoundError} +}); + +/** + * Find all instances of Model, matched by query + * make sure you have marked as `index: true` fields for filter or sort + * + * @param {Object} params (optional) + * + * - where: Object `{ key: val, key2: {gt: 'val2'}}` + * - include: String, Object or Array. See DataModel.include documentation. + * - order: String + * - limit: Number + * - skip: Number + * + * @param {Function} callback (required) called with arguments: + * + * - err (null or Error) + * - Array of instances + */ + +DataModel.find = function find(params, cb) { + throwNotAttached(this.modelName, 'find'); +}; + +// all ~ remoting attributes +setRemoting(DataModel.find, { + description: 'Find all instances of the model matched by filter from the data source', + accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'}, + returns: {arg: 'data', type: 'array', root: true}, + http: {verb: 'get', path: '/'} +}); + +/** + * Find one record, same as `all`, limited by 1 and return object, not collection + * + * @param {Object} params - search conditions: {where: {test: 'me'}} + * @param {Function} cb - callback called with (err, instance) + */ + +DataModel.findOne = function findOne(params, cb) { + throwNotAttached(this.modelName, 'findOne'); +}; + +setRemoting(DataModel.findOne, { + description: 'Find first instance of the model matched by filter from the data source', + accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'get', path: '/findOne'} +}); + +/** + * Destroy all matching records + * @param {Object} [where] An object that defines the criteria + * @param {Function} [cb] - callback called with (err) + */ + +DataModel.remove = +DataModel.deleteAll = +DataModel.destroyAll = function destroyAll(where, cb) { + throwNotAttached(this.modelName, 'destroyAll'); +}; + +/** + * Destroy a record by id + * @param {*} id The id value + * @param {Function} cb - callback called with (err) + */ + +DataModel.removeById = +DataModel.deleteById = +DataModel.destroyById = function deleteById(id, cb) { + throwNotAttached(this.modelName, 'deleteById'); +}; + +// deleteById ~ remoting attributes +setRemoting(DataModel.deleteById, { + description: 'Delete a model instance by id from the data source', + accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, + http: {verb: 'del', path: '/:id'} +}); + +/** + * Return count of matched records + * + * @param {Object} where - search conditions (optional) + * @param {Function} cb - callback, called with (err, count) + */ + +DataModel.count = function (where, cb) { + throwNotAttached(this.modelName, 'count'); +}; + +// count ~ remoting attributes +setRemoting(DataModel.count, { + description: 'Count instances of the model matched by where from the data source', + accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, + returns: {arg: 'count', type: 'number'}, + http: {verb: 'get', path: '/count'} +}); + +/** + * Save instance. When instance haven't id, create method called instead. + * Triggers: validate, save, update | create + * @param options {validate: true, throws: false} [optional] + * @param callback(err, obj) + */ + +DataModel.prototype.save = function (options, callback) { + var inst = this; + var DataModel = inst.constructor; + + if(typeof options === 'function') { + callback = options; + options = {}; + } + + // delegates directly to DataAccess + DataAccess.prototype.save.call(this, options, function(err, data) { + if(err) return callback(data); + var saved = new DataModel(data); + inst.setId(saved.getId()); + callback(null, data); + }); +}; + + +/** + * Determine if the data model is new. + * @returns {Boolean} + */ + +DataModel.prototype.isNewRecord = function () { + throwNotAttached(this.constructor.modelName, 'isNewRecord'); +}; + +/** + * Delete object from persistence + * + * @triggers `destroy` hook (async) before and after destroying object + */ + +DataModel.prototype.remove = +DataModel.prototype.delete = +DataModel.prototype.destroy = function (cb) { + throwNotAttached(this.constructor.modelName, 'destroy'); +}; + +/** + * Update single attribute + * + * equals to `updateAttributes({name: value}, cb) + * + * @param {String} name - name of property + * @param {Mixed} value - value of property + * @param {Function} callback - callback called with (err, instance) + */ + +DataModel.prototype.updateAttribute = function updateAttribute(name, value, callback) { + throwNotAttached(this.constructor.modelName, 'updateAttribute'); +}; + +/** + * Update set of attributes + * + * this method performs validation before updating + * + * @trigger `validation`, `save` and `update` hooks + * @param {Object} data - data to update + * @param {Function} callback - callback called with (err, instance) + */ + +DataModel.prototype.updateAttributes = function updateAttributes(data, cb) { + throwNotAttached(this.modelName, 'updateAttributes'); +}; + +// updateAttributes ~ remoting attributes +setRemoting(DataModel.prototype.updateAttributes, { + description: 'Update attributes for a model instance and persist it into the data source', + accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, + returns: {arg: 'data', type: 'object', root: true}, + http: {verb: 'put', path: '/'} +}); + +/** + * Reload object from persistence + * + * @requires `id` member of `object` to be able to call `find` + * @param {Function} callback - called with (err, instance) arguments + */ + +DataModel.prototype.reload = function reload(callback) { + throwNotAttached(this.constructor.modelName, 'reload'); +}; + +/** + * Set the corret `id` property for the `DataModel`. If a `Connector` defines + * a `setId` method it will be used. Otherwise the default lookup is used. You + * should override this method to handle complex ids. + * + * @param {*} val The `id` value. Will be converted to the type the id property + * specifies. + */ + +DataModel.prototype.setId = function(val) { + var ds = this.getDataSource(); + this[this.getIdName()] = val; +} + +DataModel.prototype.getId = function() { + var data = this.toObject(); + if(!data) return; + return data[this.getIdName()]; +} + +/** + * Get the id property name of the constructor. + */ + +DataModel.prototype.getIdName = function() { + return this.constructor.getIdName(); +} + +/** + * Get the id property name + */ + +DataModel.getIdName = function() { + var Model = this; + var ds = Model.getDataSource(); + + if(ds.idName) { + return ds.idName(Model.modelName); + } else { + return 'id'; + } +} diff --git a/lib/models/email.js b/lib/models/email.js index cc351c92..3950db6e 100644 --- a/lib/models/email.js +++ b/lib/models/email.js @@ -18,11 +18,11 @@ var properties = { * * **Properties** * - * - `to` - **{ String }** **required** - * - `from` - **{ String }** **required** - * - `subject` - **{ String }** **required** - * - `text` - **{ String }** - * - `html` - **{ String }** + * - `to` - String (required) + * - `from` - String (required) + * - `subject` - String (required) + * - `text` - String + * - `html` - String * * @class * @inherits {Model} @@ -35,19 +35,24 @@ var Email = module.exports = Model.extend('Email', properties); * * Example Options: * - * ```json + * ```js * { - * from: "Fred Foo ✔ ", // sender address + * from: "Fred Foo ", // sender address * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers - * subject: "Hello ✔", // Subject line - * text: "Hello world ✔", // plaintext body - * html: "Hello world ✔" // html body + * subject: "Hello", // Subject line + * text: "Hello world", // plaintext body + * html: "Hello world" // html body * } * ``` * * See https://github.com/andris9/Nodemailer for other supported options. * - * @param {Object} options + * @options {Object} options See below + * @prop {String} from Senders's email address + * @prop {String} to List of one or more recipient email addresses (comma-delimited) + * @prop {String} subject Subject line + * @prop {String} text Body text + * @prop {String} html Body HTML (optional) * @param {Function} callback Called after the e-mail is sent or the sending failed */ diff --git a/lib/models/model.js b/lib/models/model.js index 8cc25832..6365a9c0 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -2,6 +2,7 @@ * Module Dependencies. */ var loopback = require('../loopback'); +var compat = require('../compat'); var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder; var modeler = new ModelBuilder(); var async = require('async'); @@ -133,7 +134,8 @@ Model.setup = function () { var self = this; if(this.app) { var remotes = this.app.remotes(); - remotes.before(self.pluralModelName + '.' + name, function (ctx, next) { + var className = compat.getClassNameForRemoting(self); + remotes.before(className + '.' + name, function (ctx, next) { fn(ctx, ctx.result, next); }); } else { @@ -149,7 +151,8 @@ Model.setup = function () { var self = this; if(this.app) { var remotes = this.app.remotes(); - remotes.after(self.pluralModelName + '.' + name, function (ctx, next) { + var className = compat.getClassNameForRemoting(self); + remotes.after(className + '.' + name, function (ctx, next) { fn(ctx, ctx.result, next); }); } else { @@ -161,8 +164,9 @@ Model.setup = function () { }; // Map the prototype method to /:id with data in the body + var idDesc = ModelCtor.modelName + ' id'; ModelCtor.sharedCtor.accepts = [ - {arg: 'id', type: 'any', http: {source: 'path'}} + {arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc} // {arg: 'instance', type: 'object', http: {source: 'body'}} ]; @@ -185,10 +189,20 @@ Model.setup = function () { /*! * Get the reference to ACL in a lazy fashion to avoid race condition in require */ -var ACL = null; -function getACL() { - return ACL || (ACL = require('./acl').ACL); -} +var _aclModel = null; +Model._ACL = function getACL(ACL) { + if(ACL !== undefined) { + // The function is used as a setter + _aclModel = ACL; + } + if(_aclModel) { + return _aclModel; + } + var aclModel = require('./acl').ACL; + _aclModel = loopback.getModelByType(aclModel); + return _aclModel; +}; + /** * Check if the given access token can invoke the method @@ -206,9 +220,9 @@ function getACL() { Model.checkAccess = function(token, modelId, method, callback) { var ANONYMOUS = require('./access-token').ANONYMOUS; token = token || ANONYMOUS; - var ACL = getACL(); + var aclModel = Model._ACL(); var methodName = 'string' === typeof method? method: method && method.name; - ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback); + aclModel.checkAccessForToken(token, this.modelName, modelId, methodName, callback); }; /*! @@ -227,7 +241,7 @@ Model._getAccessTypeForMethod = function(method) { 'method is a required argument and must be a RemoteMethod object' ); - var ACL = getACL(); + var ACL = Model._ACL(); switch(method.name) { case'create': @@ -259,6 +273,72 @@ Model._getAccessTypeForMethod = function(method) { } } +/** + * Get the `Application` the Model is attached to. + * + * @callback {Function} callback + * @param {Error} err + * @param {Application} app + * @end + */ + +Model.getApp = function(callback) { + var Model = this; + if(this.app) { + callback(null, this.app); + } else { + Model.once('attached', function() { + assert(Model.app); + callback(null, Model.app); + }); + } +} + +/** + * Get the Model's `RemoteObjects`. + * + * @callback {Function} callback + * @param {Error} err + * @param {RemoteObjects} remoteObjects + * @end + */ + +Model.remotes = function(callback) { + this.getApp(function(err, app) { + callback(null, app.remotes()); + }); +} + +/*! + * Create a proxy function for invoking remote methods. + * + * @param {SharedMethod} sharedMethod + */ + +Model.createProxyMethod = function createProxyFunction(remoteMethod) { + var Model = this; + var scope = remoteMethod.isStatic ? Model : Model.prototype; + var original = scope[remoteMethod.name]; + + var fn = scope[remoteMethod.name] = function proxy() { + var args = Array.prototype.slice.call(arguments); + var lastArgIsFunc = typeof args[args.length - 1] === 'function'; + var callback; + if(lastArgIsFunc) { + callback = args.pop(); + } + + Model.remotes(function(err, remotes) { + remotes.invoke(remoteMethod.stringName, args, callback); + }); + } + + for(var key in original) { + fn[key] = original[key]; + } + fn._delegate = true; +} + // setup the initial model Model.setup(); diff --git a/lib/models/oauth2.js b/lib/models/oauth2.js deleted file mode 100644 index ba82a6e3..00000000 --- a/lib/models/oauth2.js +++ /dev/null @@ -1,222 +0,0 @@ -var loopback = require('../loopback'); - -// "OAuth token" -var OAuthToken = loopback.createModel({ - // "access token" - accessToken: { - type: String, - index: { - unique: true - } - }, // key, The string token - clientId: { - type: String, - index: true - }, // The client id - resourceOwner: { - type: String, - index: true - }, // The resource owner (user) id - realm: { - type: String, - index: true - }, // The resource owner realm - issuedAt: { - type: Date, - index: true - }, // The timestamp when the token is issued - expiresIn: Number, // Expiration time in seconds - expiredAt: { - type: Date, - index: { - expires: "1d" - } - }, // The timestamp when the token is expired - scopes: [ String ], // oAuth scopes - parameters: [ - { - name: String, - value: String - } - ], - - authorizationCode: { - type: String, - index: true - }, // The corresponding authorization code that is used to request the - // access token - refreshToken: { - type: String, - index: true - }, // The corresponding refresh token if issued - - tokenType: { - type: String, - enum: [ "Bearer", "MAC" ] - }, // The token type, such as Bearer: - // http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16 - // or MAC: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 - authenticationScheme: String, // HTTP authenticationScheme - hash: String // The SHA-1 hash for -// client-secret/resource-owner-secret-key -}); - -// "OAuth authorization code" -var OAuthAuthorizationCode = loopback.createModel({ - code: { - type: String, - index: { - unique: true - } - }, // key // The string code - clientId: { - type: String, - index: true - }, // The client id - resourceOwner: { - type: String, - index: true - }, // The resource owner (user) id - realm: { - type: String, - index: true - }, // The resource owner realm - - issuedAt: { - type: Date, - index: true - }, // The timestamp when the token is issued - expiresIn: Number, // Expiration time in seconds - expiredAt: { - type: Date, - index: { - expires: "1d" - } - }, // The timestamp when the token is expired - - scopes: [ String ], // oAuth scopes - parameters: [ - { - name: String, - value: String - } - ], - - used: Boolean, // Is it ever used - redirectURI: String, // The redirectURI from the request, we need to - // check if it's identical to the one used for - // access token - hash: String // The SHA-1 hash for -// client-secret/resource-owner-secret-key -}); - -// "OAuth client registration record" -var ClientRegistration = loopback.createModel({ - id: { - type: String, - index: { - unique: true - } - }, - clientId: { - type: String, - index: { - unique: true - } - }, // key; // The client id - clientSecret: String, // The generated client secret - - defaultTokenType: String, - accessLevel: Number, // The access level to scopes, -1: disabled, 0: - // basic, 1..N - disabled: Boolean, - - name: { - type: String, - index: true - }, - email: String, - description: String, - url: String, - iconURL: String, - redirectURIs: [ String ], - type: { - type: String, - enum: [ "CONFIDENTIAL", "PUBLIC" ] - }, - - userId: { - type: String, - index: true - } // The registered developer - -}); - -// "OAuth permission" -var OAuthPermission = loopback.createModel({ - clientId: { - type: String, - index: true - }, // The client id - resourceOwner: { - type: String, - index: true - }, // The resource owner (user) id - realm: { - type: String, - index: true - }, // The resource owner realm - - issuedAt: { - type: Date, - index: true - }, // The timestamp when the permission is issued - expiresIn: Number, // Expiration time in seconds - expiredAt: { - type: Date, - index: { - expires: "1d" - } - }, // The timestamp when the permission is expired - - scopes: [ String ] -}); - -// "OAuth scope" -var OAuthScope = loopback.createModel({ - scope: { - type: String, - index: { - unique: true - } - }, // key; // The scope name - description: String, // Description of the scope - iconURL: String, // The icon to be displayed on the "Request Permission" - // dialog - expiresIn: Number, // The default maximum lifetime of access token that - // carries the scope - requiredAccessLevel: Number, // The minimum access level required - resourceOwnerAuthorizationRequired: Boolean -// The scope requires authorization from the resource owner -}); - -// "OAuth protected resource" -var OAuthResource = loopback.createModel({ - operations: [ - { - type: String, - enum: [ "ALL", "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH" ] - } - ], // A list of operations, by default ALL - path: String, // The resource URI path - scopes: [ String ] -// Allowd scopes -}); - -// Use the schema to register a model -exports.OAuthToken = OAuthToken; -exports.OAuthAuthorizationCode = OAuthAuthorizationCode; -exports.ClientRegistration = ClientRegistration; -exports.OAuthPermission = OAuthPermission; -exports.OAuthScope = OAuthScope; -exports.OAuthResource = OAuthResource; diff --git a/lib/models/role.js b/lib/models/role.js index e249f780..86ff5cc3 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -16,7 +16,7 @@ var RoleSchema = { modified: {type: Date, default: Date} }; -/** +/*! * Map principals to roles */ var RoleMappingSchema = { @@ -27,7 +27,10 @@ var RoleMappingSchema = { }; /** - * Map Roles to + * The `RoleMapping` model extends from the built in `loopback.Model` type. + * + * @class + * @inherits {Model} */ var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, { @@ -196,6 +199,21 @@ function isUserClass(modelClass) { modelClass.prototype instanceof loopback.User; } +/*! + * Check if two user ids matches + * @param {*} id1 + * @param {*} id2 + * @returns {boolean} + */ +function matches(id1, id2) { + if (id1 === undefined || id1 === null || id1 ==='' + || id2 === undefined || id2 === null || id2 === '') { + return false; + } + // The id can be a MongoDB ObjectID + return id1 === id2 || id1.toString() === id2.toString(); +} + /** * Check if a given userId is the owner the model instance * @param {Function} modelClass The model class @@ -205,7 +223,7 @@ function isUserClass(modelClass) { */ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) { assert(modelClass, 'Model class is required'); - debug('isOwner(): %s %s %s', modelClass && modelClass.modelName, modelId, userId); + debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId); // No userId is present if(!userId) { process.nextTick(function() { @@ -217,19 +235,21 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) { // Is the modelClass User or a subclass of User? if(isUserClass(modelClass)) { process.nextTick(function() { - callback(null, modelId == userId); + callback(null, matches(modelId, userId)); }); return; } modelClass.findById(modelId, function(err, inst) { if(err || !inst) { + debug('Model not found for id %j', modelId); callback && callback(err, false); return; } debug('Model found: %j', inst); - if(inst.userId || inst.owner) { - callback && callback(null, (inst.userId || inst.owner) === userId); + var ownerId = inst.userId || inst.owner; + if(ownerId) { + callback && callback(null, matches(ownerId, userId)); return; } else { // Try to follow belongsTo @@ -240,7 +260,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) { inst[r](function(err, user) { if(!err && user) { debug('User found: %j', user.id); - callback && callback(null, user.id === userId); + callback && callback(null, matches(user.id, userId)); } else { callback && callback(err, false); } @@ -248,6 +268,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) { return; } } + debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId); callback && callback(null, false); } }); @@ -401,6 +422,9 @@ Role.getRoles = function (context, callback) { Object.keys(Role.resolvers).forEach(function (role) { inRoleTasks.push(function (done) { self.isInRole(role, context, function (err, inRole) { + if(debug.enabled) { + debug('In role %j: %j', role, inRole); + } if (!err && inRole) { addRole(role); done(); diff --git a/lib/models/user.js b/lib/models/user.js index 5bd2b102..374fe7c4 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -1,4 +1,4 @@ -/** +/*! * Module Dependencies. */ @@ -18,7 +18,9 @@ var Model = require('../loopback').Model , ACL = require('./acl').ACL , assert = require('assert'); -/** +var debug = require('debug')('loopback:user'); + +/*! * Default User properties. */ @@ -48,11 +50,12 @@ var properties = { }; var options = { + hidden: ['password'], acls: [ { principalType: ACL.ROLE, principalId: Role.EVERYONE, - permission: ACL.DENY, + permission: ACL.DENY }, { principalType: ACL.ROLE, @@ -89,15 +92,28 @@ var options = { principalId: Role.OWNER, permission: ACL.ALLOW, property: "updateAttributes" + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.ALLOW, + property: "confirm" } - ] + ], + relations: { + accessTokens: { + type: 'hasMany', + model: 'AccessToken', + foreignKey: 'userId' + } + } }; /** * Extends from the built in `loopback.Model` type. * * Default `User` ACLs. - * + * * - DENY EVERYONE `*` * - ALLOW EVERYONE `create` * - ALLOW OWNER `removeById` @@ -127,40 +143,64 @@ var User = module.exports = Model.extend('User', properties, options); * @param {AccessToken} token */ -User.login = function (credentials, fn) { - var UserCtor = this; +User.login = function (credentials, include, fn) { + if (typeof include === 'function') { + fn = include; + include = undefined; + } + + include = (include || '').toLowerCase(); + var query = {}; - if(credentials.email) { query.email = credentials.email; } else if(credentials.username) { query.username = credentials.username; } else { - return fn(new Error('must provide username or email')); + var err = new Error('username or email is required'); + err.statusCode = 400; + return fn(err); } - + this.findOne({where: query}, function(err, user) { var defaultError = new Error('login failed'); - + defaultError.statusCode = 401; + if(err) { + debug('An error is reported from User.findOne: %j', err); fn(defaultError); } else if(user) { user.hasPassword(credentials.password, function(err, isMatch) { if(err) { + debug('An error is reported from User.hasPassword: %j', err); fn(defaultError); } else if(isMatch) { user.accessTokens.create({ ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL) - }, fn); + }, function(err, token) { + if (err) return fn(err); + if (include === 'user') { + // NOTE(bajtos) We can't set token.user here: + // 1. token.user already exists, it's a function injected by + // "AccessToken belongsTo User" relation + // 2. ModelBaseClass.toJSON() ignores own properties, thus + // the value won't be included in the HTTP response + // See also loopback#161 and loopback#162 + token.__data.user = user; + } + fn(err, token); + }); } else { + debug('The password is invalid for user %s', query.email || query.username); fn(defaultError); } }); } else { + debug('No matching record is found for user %s', query.email || query.username); fn(defaultError); } }); -} +}; /** * Logout a user with the given accessToken id. @@ -230,7 +270,7 @@ User.prototype.verify = function (options, fn) { assert(options.type === 'email', 'Unsupported verification type'); assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); - + options.redirect = options.redirect || '/'; options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); options.user = this; @@ -240,40 +280,44 @@ User.prototype.verify = function (options, fn) { options.protocol + '://' + options.host - + (User.sharedCtor.http.path || '/' + User.pluralModelName) - + User.confirm.http.path; - + + User.http.path + + User.confirm.http.path + + '?uid=' + + options.user.id + + '&redirect=' + + options.redirect; + - // Email model var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); - + crypto.randomBytes(64, function(err, buf) { if(err) { fn(err); } else { - user.verificationToken = buf.toString('base64'); + user.verificationToken = buf.toString('hex'); user.save(function (err) { if(err) { fn(err); } else { - sendEmail(user); + sendEmail(user); } }); } }); - + // TODO - support more verification types function sendEmail(user) { - options.verifyHref += '?token=' + user.verificationToken; - + options.verifyHref += '&token=' + user.verificationToken; + options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; - + options.text = options.text.replace('{href}', options.verifyHref); - + var template = loopback.template(options.template); Email.send({ to: options.to || user.email, + from: options.from, subject: options.subject || 'Thanks for Registering', text: options.text, html: template(options) @@ -372,7 +416,7 @@ User.setup = function () { // We need to call the base class's setup method Model.setup.call(this); var UserModel = this; - + // max ttl this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; this.settings.ttl = DEFAULT_TTL; @@ -381,18 +425,27 @@ User.setup = function () { var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); this.$password = bcrypt.hashSync(plain, salt); } - + loopback.remoteMethod( UserModel.login, { accepts: [ - {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}} + {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, + {arg: 'include', type: 'string', http: {source: 'query' }, description: + 'Related objects to include in the response. ' + + 'See the description of return value for more details.'} ], - returns: {arg: 'accessToken', type: 'object', root: true}, + returns: { + arg: 'accessToken', type: 'object', root: true, description: + 'The response body contains properties of the AccessToken created on login.\n' + + 'Depending on the value of `include` parameter, the body may contain ' + + 'additional properties:\n\n' + + ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' + }, http: {verb: 'post'} } ); - + loopback.remoteMethod( UserModel.logout, { @@ -403,12 +456,15 @@ User.setup = function () { var tokenID = accessToken && accessToken.id; return tokenID; - }} + }, description: + 'Do not supply this argument, it is automatically extracted ' + + 'from request headers.' + } ], http: {verb: 'all'} } ); - + loopback.remoteMethod( UserModel.confirm, { @@ -420,7 +476,7 @@ User.setup = function () { http: {verb: 'get', path: '/confirm'} } ); - + loopback.remoteMethod( UserModel.resetPassword, { @@ -440,16 +496,16 @@ User.setup = function () { } }); }); - + // default models UserModel.email = require('./email'); UserModel.accessToken = require('./access-token'); - + UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - + UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); - + return UserModel; } diff --git a/package.json b/package.json index ef2b38a5..b1b223e8 100644 --- a/package.json +++ b/package.json @@ -9,40 +9,65 @@ "Platform", "mBaaS" ], - "version": "1.5.3", + "version": "1.7.7", "scripts": { "test": "mocha -R spec" }, "dependencies": { - "debug": "~0.7.2", - "express": "~3.4.0", - "strong-remoting": "~1.1.0", - "inflection": "~1.2.5", - "passport": "~0.1.17", + "debug": "~0.7.4", + "express": "~3.4.8", + "strong-remoting": "~1.3.1", + "inflection": "~1.3.5", + "passport": "~0.2.0", "passport-local": "~0.1.6", - "nodemailer": "~0.5.7", - "ejs": "~0.8.4", - "bcryptjs": "~0.7.10", + "nodemailer": "~0.6.0", + "ejs": "~0.8.5", + "bcryptjs": "~0.7.12", "underscore.string": "~2.3.3", - "underscore": "~1.5.2", + "underscore": "~1.6.0", "uid2": "0.0.3", - "async": "~0.2.9", + "async": "~0.2.10", "canonical-json": "0.0.3" }, "peerDependencies": { - "loopback-datasource-juggler": "~1.2.11" + "loopback-datasource-juggler": "~1.3.11" }, "devDependencies": { - "loopback-datasource-juggler": "~1.2.11", - "mocha": "~1.14.0", + "loopback-datasource-juggler": "~1.3.11", + "mocha": "~1.17.1", "strong-task-emitter": "0.0.x", - "supertest": "~0.8.1", - "chai": "~1.8.1", - "loopback-testing": "~0.1.0" + "supertest": "~0.9.0", + "chai": "~1.9.0", + "loopback-testing": "~0.1.2", + "browserify": "~3.41.0", + "grunt": "~0.4.2", + "grunt-browserify": "~1.3.1", + "grunt-contrib-uglify": "~0.3.2", + "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-watch": "~0.5.3", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.2", + "karma-firefox-launcher": "~0.1.3", + "karma-html2js-preprocessor": "~0.1.0", + "karma-phantomjs-launcher": "~0.1.2", + "karma": "~0.10.9", + "karma-browserify": "~0.2.0", + "karma-mocha": "~0.1.1", + "grunt-karma": "~0.6.2" }, "repository": { "type": "git", "url": "https://github.com/strongloop/loopback" }, - "license": "MIT" + "browser": { + "express": "./lib/browser-express.js", + "connect": false, + "passport": false, + "passport-local": false, + "nodemailer": false + }, + "license": { + "name": "Dual MIT/StrongLoop", + "url": "https://github.com/strongloop/loopback/blob/master/LICENSE" + } } diff --git a/test/README.md b/test/README.md deleted file mode 100644 index 15ca0530..00000000 --- a/test/README.md +++ /dev/null @@ -1,1139 +0,0 @@ -# TOC - - [app](#app) - - [app.model(Model)](#app-appmodelmodel) - - [app.models()](#app-appmodels) -<<<<<<< HEAD -======= - - [loopback](#loopback) - - [loopback.createDataSource(options)](#loopback-loopbackcreatedatasourceoptions) - - [loopback.remoteMethod(Model, fn, [options]);](#loopback-loopbackremotemethodmodel-fn-options) ->>>>>>> master - - [DataSource](#datasource) - - [dataSource.createModel(name, properties, settings)](#datasource-datasourcecreatemodelname-properties-settings) - - [dataSource.operations()](#datasource-datasourceoperations) - - [GeoPoint](#geopoint) - - [geoPoint.distanceTo(geoPoint, options)](#geopoint-geopointdistancetogeopoint-options) - - [GeoPoint.distanceBetween(a, b, options)](#geopoint-geopointdistancebetweena-b-options) - - [GeoPoint()](#geopoint-geopoint) - - [loopback](#loopback) - - [loopback.createDataSource(options)](#loopback-loopbackcreatedatasourceoptions) - - [loopback.remoteMethod(Model, fn, [options]);](#loopback-loopbackremotemethodmodel-fn-options) - - [loopback.memory([name])](#loopback-loopbackmemoryname) - - [Memory Connector](#memory-connector) - - [Model](#model) - - [Model.validatesPresenceOf(properties...)](#model-modelvalidatespresenceofproperties) - - [Model.validatesLengthOf(property, options)](#model-modelvalidateslengthofproperty-options) - - [Model.validatesInclusionOf(property, options)](#model-modelvalidatesinclusionofproperty-options) - - [Model.validatesExclusionOf(property, options)](#model-modelvalidatesexclusionofproperty-options) - - [Model.validatesNumericalityOf(property, options)](#model-modelvalidatesnumericalityofproperty-options) - - [Model.validatesUniquenessOf(property, options)](#model-modelvalidatesuniquenessofproperty-options) - - [myModel.isValid()](#model-mymodelisvalid) - - [Model.attachTo(dataSource)](#model-modelattachtodatasource) - - [Model.create([data], [callback])](#model-modelcreatedata-callback) - - [model.save([options], [callback])](#model-modelsaveoptions-callback) - - [model.updateAttributes(data, [callback])](#model-modelupdateattributesdata-callback) - - [Model.upsert(data, callback)](#model-modelupsertdata-callback) - - [model.destroy([callback])](#model-modeldestroycallback) - - [Model.destroyAll(callback)](#model-modeldestroyallcallback) - - [Model.findById(id, callback)](#model-modelfindbyidid-callback) - - [Model.count([query], callback)](#model-modelcountquery-callback) - - [Remote Methods](#model-remote-methods) - - [Example Remote Method](#model-remote-methods-example-remote-method) - - [Model.beforeRemote(name, fn)](#model-remote-methods-modelbeforeremotename-fn) - - [Model.afterRemote(name, fn)](#model-remote-methods-modelafterremotename-fn) - - [Remote Method invoking context](#model-remote-methods-remote-method-invoking-context) - - [ctx.req](#model-remote-methods-remote-method-invoking-context-ctxreq) - - [ctx.res](#model-remote-methods-remote-method-invoking-context-ctxres) - - [Model.hasMany(Model)](#model-modelhasmanymodel) - - [Model.properties](#model-modelproperties) - - [Model.extend()](#model-modelextend) - - [User](#user) - - [User.create](#user-usercreate) - - [User.login](#user-userlogin) - - [User.logout](#user-userlogout) - - [user.hasPassword(plain, fn)](#user-userhaspasswordplain-fn) - - [Verification](#user-verification) - - [user.verify(options, fn)](#user-verification-userverifyoptions-fn) - - [User.confirm(options, fn)](#user-verification-userconfirmoptions-fn) - - - -# app - -## app.model(Model) -Expose a `Model` to remote clients. - -```js -var memory = loopback.createDataSource({connector: loopback.Memory}); -var Color = memory.createModel('color', {name: String}); -app.model(Color); -assert.equal(app.models().length, 1); -``` - - -## app.models() -Get the app's exposed models. - -```js -var Color = loopback.createModel('color', {name: String}); -var models = app.models(); - -assert.equal(models.length, 1); -assert.equal(models[0].modelName, 'color'); -``` - -<<<<<<< HEAD -======= - -# loopback - -## loopback.createDataSource(options) -Create a data source with a connector.. - -```js -var dataSource = loopback.createDataSource({ - connector: loopback.Memory -}); -assert(dataSource.connector()); -``` - - -## loopback.remoteMethod(Model, fn, [options]); -Setup a remote method.. - -```js -var Product = loopback.createModel('product', {price: Number}); - -Product.stats = function(fn) { - // ... -} - -loopback.remoteMethod( - Product.stats, - { - returns: {arg: 'stats', type: 'array'}, - http: {path: '/info', verb: 'get'} - } -); - -assert.equal(Product.stats.returns.arg, 'stats'); -assert.equal(Product.stats.returns.type, 'array'); -assert.equal(Product.stats.http.path, '/info'); -assert.equal(Product.stats.http.verb, 'get'); -assert.equal(Product.stats.shared, true); -``` - ->>>>>>> master - -# DataSource - -## dataSource.createModel(name, properties, settings) -Define a model and attach it to a `DataSource`. - -```js -var Color = memory.createModel('color', {name: String}); -assert.isFunc(Color, 'find'); -assert.isFunc(Color, 'findById'); -assert.isFunc(Color, 'findOne'); -assert.isFunc(Color, 'create'); -assert.isFunc(Color, 'updateOrCreate'); -assert.isFunc(Color, 'upsert'); -assert.isFunc(Color, 'findOrCreate'); -assert.isFunc(Color, 'exists'); -assert.isFunc(Color, 'destroyAll'); -assert.isFunc(Color, 'count'); -assert.isFunc(Color, 'include'); -assert.isFunc(Color, 'relationNameFor'); -assert.isFunc(Color, 'hasMany'); -assert.isFunc(Color, 'belongsTo'); -assert.isFunc(Color, 'hasAndBelongsToMany'); -assert.isFunc(Color.prototype, 'save'); -assert.isFunc(Color.prototype, 'isNewRecord'); -assert.isFunc(Color.prototype, 'destroy'); -assert.isFunc(Color.prototype, 'updateAttribute'); -assert.isFunc(Color.prototype, 'updateAttributes'); -assert.isFunc(Color.prototype, 'reload'); -``` - - -## dataSource.operations() -List the enabled and disabled operations. - -```js -// assert the defaults -// - true: the method should be remote enabled -// - false: the method should not be remote enabled -// - -existsAndShared('_forDB', false); -existsAndShared('create', true); -existsAndShared('updateOrCreate', false); -existsAndShared('upsert', false); -existsAndShared('findOrCreate', false); -existsAndShared('exists', true); -existsAndShared('find', true); -existsAndShared('findOne', true); -existsAndShared('destroyAll', false); -existsAndShared('count', true); -existsAndShared('include', false); -existsAndShared('relationNameFor', false); -existsAndShared('hasMany', false); -existsAndShared('belongsTo', false); -existsAndShared('hasAndBelongsToMany', false); -existsAndShared('save', true); -existsAndShared('isNewRecord', false); -existsAndShared('_adapter', false); -existsAndShared('destroy', true); -existsAndShared('updateAttributes', true); -existsAndShared('reload', true); - -function existsAndShared(name, isRemoteEnabled) { - var op = memory.getOperation(name); - assert(op.remoteEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled'); -} -``` - - -# GeoPoint - -## geoPoint.distanceTo(geoPoint, options) -Get the distance to another `GeoPoint`. - -```js -var here = new GeoPoint({lat: 10, lng: 10}); -var there = new GeoPoint({lat: 5, lng: 5}); - -assert.equal(here.distanceTo(there, {type: 'meters'}), 782777.923052584); -``` - - -## GeoPoint.distanceBetween(a, b, options) -Get the distance between two points. - -```js -var here = new GeoPoint({lat: 10, lng: 10}); -var there = new GeoPoint({lat: 5, lng: 5}); - -assert.equal(GeoPoint.distanceBetween(here, there, {type: 'feet'}), 2568169.038886431); -``` - - -## GeoPoint() -Create from string. - -```js -var point = new GeoPoint('1.234,5.678'); -assert.equal(point.lng, 1.234); -assert.equal(point.lat, 5.678); -var point2 = new GeoPoint('1.222, 5.333'); -assert.equal(point2.lng, 1.222); -assert.equal(point2.lat, 5.333); -var point3 = new GeoPoint('1.333, 5.111'); -assert.equal(point3.lng, 1.333); -assert.equal(point3.lat, 5.111); -``` - -Serialize as string. - -```js -var str = '1.234,5.678'; -var point = new GeoPoint(str); -assert.equal(point.toString(), str); -``` - -Create from array. - -```js -var point = new GeoPoint([5.555, 6.777]); -assert.equal(point.lng, 5.555); -assert.equal(point.lat, 6.777); -``` - -Create as Model property. - -```js -var Model = loopback.createModel('geo-model', { - geo: {type: 'GeoPoint'} -}); - -var m = new Model({ - geo: '1.222,3.444' -}); - -assert(m.geo instanceof GeoPoint); -assert.equal(m.geo.lng, 1.222); -assert.equal(m.geo.lat, 3.444); -``` - - -# loopback - -## loopback.createDataSource(options) -Create a data source with a connector. - -```js -var dataSource = loopback.createDataSource({ - connector: loopback.Memory -}); -assert(dataSource.connector()); -``` - - -## loopback.remoteMethod(Model, fn, [options]); -Setup a remote method. - -```js -var Product = loopback.createModel('product', {price: Number}); - -Product.stats = function(fn) { - // ... -} - -loopback.remoteMethod( - Product.stats, - { - returns: {arg: 'stats', type: 'array'}, - http: {path: '/info', verb: 'get'} - } -); - -assert.equal(Product.stats.returns.arg, 'stats'); -assert.equal(Product.stats.returns.type, 'array'); -assert.equal(Product.stats.http.path, '/info'); -assert.equal(Product.stats.http.verb, 'get'); -assert.equal(Product.stats.shared, true); -``` - - -## loopback.memory([name]) -Get an in-memory data source. Use one if it already exists. - -```js -var memory = loopback.memory(); -assertValidDataSource(memory); -var m1 = loopback.memory(); -var m2 = loopback.memory('m2'); -var alsoM2 = loopback.memory('m2'); - -assert(m1 === memory); -assert(m1 !== m2); -assert(alsoM2 === m2); -``` - - -# Memory Connector -Create a model using the memory connector. - -```js -// use the built in memory function -// to create a memory data source -var memory = loopback.memory(); - -// or create it using the standard -// data source creation api -var memory = loopback.createDataSource({ - connector: loopback.Memory -}); - -// create a model using the -// memory data source -var properties = { - name: String, - price: Number -}; - -var Product = memory.createModel('product', properties); - -Product.create([ - {name: 'apple', price: 0.79}, - {name: 'pear', price: 1.29}, - {name: 'orange', price: 0.59}, -], count); - -function count() { - Product.count(function (err, count) { - assert.equal(count, 3); - done(); - }); -} -``` - - -# Model - -## Model.validatesPresenceOf(properties...) -Require a model to include a property to be considered valid. - -```js -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'); -``` - - -## Model.validatesLengthOf(property, options) -Require a property length to be within a specified range. - -```js -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'); -``` - - -## Model.validatesInclusionOf(property, options) -Require a value for `property` to be in the specified array. - -```js -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'); -``` - - -## Model.validatesExclusionOf(property, options) -Require a value for `property` to not exist in the specified array. - -```js -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'); -``` - - -## Model.validatesNumericalityOf(property, options) -Require a value for `property` to be a specific type of `Number`. - -```js -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'); -``` - - -## Model.validatesUniquenessOf(property, options) -Ensure the value for `property` is unique. - -```js -User.validatesUniquenessOf('email', {message: 'email is not unique'}); - -var joe = new User({email: 'joe@joe.com'}); -var joe2 = new User({email: 'joe@joe.com'}); - -joe.save(function () { - joe2.save(function (err) { - assert(err, 'should get a validation error'); - assert(joe2.errors.email, 'model should have email error'); - - done(); - }); -}); -``` - - -## myModel.isValid() -Validate the model instance. - -```js -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'); -``` - -Asynchronously validate the model. - -```js -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(); -}); -``` - - -## Model.attachTo(dataSource) -Attach a model to a [DataSource](#data-source). - -```js -var MyModel = loopback.createModel('my-model', {name: String}); - -assert(MyModel.find === undefined, 'should not have data access methods'); - -MyModel.attachTo(memory); - -assert(typeof MyModel.find === 'function', 'should have data access methods after attaching to a data source'); -``` - - -## Model.create([data], [callback]) -Create an instance of Model with given data and save to the attached data source. - -```js -User.create({first: 'Joe', last: 'Bob'}, function(err, user) { - assert(user instanceof User); - done(); -}); -``` - - -## model.save([options], [callback]) -Save an instance of a Model to the attached data source. - -```js -var joe = new User({first: 'Joe', last: 'Bob'}); -joe.save(function(err, user) { - assert(user.id); - assert(!err); - assert(!user.errors); - done(); -}); -``` - - -## model.updateAttributes(data, [callback]) -Save specified attributes to the attached data source. - -```js -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(); - }); -}); -``` - - -## Model.upsert(data, callback) -Update when record with id=data.id found, insert otherwise. - -```js -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(); - }); -}); -``` - - -## model.destroy([callback]) -Remove a model from the attached data source. - -```js -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(!err); - assert.equal(notFound, null); - done(); - }); - }); - }); -}); -``` - - -## Model.destroyAll(callback) -Delete all Model instances from data source. - -```js -(new TaskEmitter()) - .task(User, 'create', {first: 'jill'}) - .task(User, 'create', {first: 'bob'}) - .task(User, 'create', {first: 'jan'}) - .task(User, 'create', {first: 'sam'}) - .task(User, 'create', {first: 'suzy'}) - .on('done', function () { - User.count(function (err, count) { - assert.equal(count, 5); - User.destroyAll(function () { - User.count(function (err, count) { - assert.equal(count, 0); - done(); - }); - }); - }); - }); -``` - - -## Model.findById(id, callback) -Find an instance by id. - -```js -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(); - }); -}); -``` - - -## Model.count([query], callback) -Query count of Model instances in data source. - -```js -(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(); - }); - }); -``` - - -## Remote Methods - -### Example Remote Method -Call the method using HTTP / REST. - -```js -request(app) - .get('/users/sign-in?username=foo&password=bar') - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res){ - if(err) return done(err); - assert(res.body.$data === 123); - done(); - }); -``` - - -### Model.beforeRemote(name, fn) -Run a function before a remote method is called by a client. - -```js -var hookCalled = false; - -User.beforeRemote('create', function(ctx, user, next) { - hookCalled = true; - next(); -}); - -// invoke save -request(app) - .post('/users') - .send({data: {first: 'foo', last: 'bar'}}) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - assert(hookCalled, 'hook wasnt called'); - done(); - }); -``` - - -### Model.afterRemote(name, fn) -Run a function after a remote method is called by a client. - -```js -var beforeCalled = false; -var afterCalled = false; - -User.beforeRemote('create', function(ctx, user, next) { - assert(!afterCalled); - beforeCalled = true; - next(); -}); -User.afterRemote('create', function(ctx, user, next) { - assert(beforeCalled); - afterCalled = true; - next(); -}); - -// invoke save -request(app) - .post('/users') - .send({data: {first: 'foo', last: 'bar'}}) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - assert(beforeCalled, 'before hook was not called'); - assert(afterCalled, 'after hook was not called'); - done(); - }); -``` - - -### Remote Method invoking context - -#### ctx.req -The express ServerRequest object. - -```js -var hookCalled = false; - -User.beforeRemote('create', function(ctx, user, next) { - hookCalled = true; - assert(ctx.req); - assert(ctx.req.url); - assert(ctx.req.method); - assert(ctx.res); - assert(ctx.res.write); - assert(ctx.res.end); - next(); -}); - -// invoke save -request(app) - .post('/users') - .send({data: {first: 'foo', last: 'bar'}}) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - assert(hookCalled); - done(); - }); -``` - - -#### ctx.res -The express ServerResponse object. - -```js -var hookCalled = false; - -User.beforeRemote('create', function(ctx, user, next) { - hookCalled = true; - assert(ctx.req); - assert(ctx.req.url); - assert(ctx.req.method); - assert(ctx.res); - assert(ctx.res.write); - assert(ctx.res.end); - next(); -}); - -// invoke save -request(app) - .post('/users') - .send({data: {first: 'foo', last: 'bar'}}) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - assert(hookCalled); - done(); - }); -``` - - -## Model.hasMany(Model) -Define a one to many relationship. - -```js -var Book = memory.createModel('book', {title: String, author: String}); -var Chapter = memory.createModel('chapter', {title: String}); - -// by referencing model -Book.hasMany(Chapter); - -Book.create({title: 'Into the Wild', author: 'Jon Krakauer'}, function(err, book) { - // using 'chapters' scope for build: - var c = book.chapters.build({title: 'Chapter 1'}); - book.chapters.create({title: 'Chapter 2'}, function () { - c.save(function () { - Chapter.count({bookId: book.id}, function (err, count) { - assert.equal(count, 2); - book.chapters({where: {title: 'Chapter 1'}}, function(err, chapters) { - assert.equal(chapters.length, 1); - assert.equal(chapters[0].title, 'Chapter 1'); - done(); - }); - }); - }); - }); -}); -``` - - -## Model.properties -Normalized properties passed in originally by loopback.createModel(). - -```js -var props = { - s: String, - n: {type: 'Number'}, - o: {type: 'String', min: 10, max: 100}, - d: Date, - g: loopback.GeoPoint -}; - -var MyModel = loopback.createModel('foo', props); - -Object.keys(MyModel.properties).forEach(function (key) { - var p = MyModel.properties[key]; - var o = MyModel.properties[key]; - assert(p); - assert(o); - assert(typeof p.type === 'function'); - - if(typeof o === 'function') { - // the normalized property - // should match the given property - assert( - p.type.name === o.name - || - p.type.name === o - ) - } -}); -``` - - -## Model.extend() -Create a new model by extending an existing model. - -```js -var User = loopback.Model.extend('test-user', { - email: String -}); - -User.foo = function () { - return 'bar'; -} - -User.prototype.bar = function () { - return 'foo'; -} - -var MyUser = User.extend('my-user', { - a: String, - b: String -}); - -assert.equal(MyUser.prototype.bar, User.prototype.bar); -assert.equal(MyUser.foo, User.foo); - -var user = new MyUser({ - email: 'foo@bar.com', - a: 'foo', - b: 'bar' -}); - -assert.equal(user.email, 'foo@bar.com'); -assert.equal(user.a, 'foo'); -assert.equal(user.b, 'bar'); -``` - - -# User - -## User.create -Create a new user. - -```js -User.create({email: 'f@b.com'}, function (err, user) { - assert(!err); - assert(user.id); - assert(user.email); - done(); -}); -``` - -Requires a valid email. - -```js -User.create({}, function (err) { - assert(err); - User.create({email: 'foo@'}, function (err) { - assert(err); - done(); - }); -}); -``` - -Requires a unique email. - -```js -User.create({email: 'a@b.com'}, function () { - User.create({email: 'a@b.com'}, function (err) { - assert(err, 'should error because the email is not unique!'); - done(); - }); -}); -``` - -Requires a password to login with basic auth. - -```js -User.create({email: 'b@c.com'}, function (err) { - User.login({email: 'b@c.com'}, function (err, session) { - assert(!session, 'should not create a session without a valid password'); - assert(err, 'should not login without a password'); - done(); - }); -}); -``` - -Hashes the given password. - -```js -var u = new User({username: 'foo', password: 'bar'}); -assert(u.password !== 'bar'); -``` - - -## User.login -Login a user by providing credentials. - -```js -request(app) - .post('/users/login') - .expect('Content-Type', /json/) - .expect(200) - .send({email: 'foo@bar.com', password: 'bar'}) - .end(function(err, res){ - if(err) return done(err); - var session = res.body; - - assert(session.uid); - assert(session.id); - assert.equal((new Buffer(session.id, 'base64')).length, 64); - - done(); - }); -``` - - -## User.logout -Logout a user by providing the current session id (using node). - -```js -login(logout); - -function login(fn) { - User.login({email: 'foo@bar.com', password: 'bar'}, fn); -} - -function logout(err, session) { - User.logout(session.id, verify(session.id, done)); -} -``` - -Logout a user by providing the current session id (over rest). - -```js -login(logout); - -function login(fn) { - request(app) - .post('/users/login') - .expect('Content-Type', /json/) - .expect(200) - .send({email: 'foo@bar.com', password: 'bar'}) - .end(function(err, res){ - if(err) return done(err); - var session = res.body; - - assert(session.uid); - assert(session.id); - - fn(null, session.id); - }); -} - -function logout(err, sid) { - request(app) - .post('/users/logout') - .expect(200) - .send({sid: sid}) - .end(verify(sid, done)); -} -``` - -Logout a user using the instance method. - -```js -login(logout); - -function login(fn) { - User.login({email: 'foo@bar.com', password: 'bar'}, fn); -} - -function logout(err, session) { - User.findOne({email: 'foo@bar.com'}, function (err, user) { - user.logout(verify(session.id, done)); - }); -} -``` - - -## user.hasPassword(plain, fn) -Determine if the password matches the stored password. - -```js -var u = new User({username: 'foo', password: 'bar'}); -u.hasPassword('bar', function (err, isMatch) { - assert(isMatch, 'password doesnt match'); - done(); -}); -``` - -should match a password when saved. - -```js -var u = new User({username: 'a', password: 'b', email: 'z@z.net'}); - -u.save(function (err, user) { - User.findById(user.id, function (err, uu) { - uu.hasPassword('b', function (err, isMatch) { - assert(isMatch); - done(); - }); - }); -}); -``` - -should match a password after it is changed. - -```js -User.create({email: 'foo@baz.net', username: 'bat', password: 'baz'}, function (err, user) { - User.findById(user.id, function (err, foundUser) { - assert(foundUser); - foundUser.hasPassword('baz', function (err, isMatch) { - assert(isMatch); - foundUser.password = 'baz2'; - foundUser.save(function (err, updatedUser) { - updatedUser.hasPassword('baz2', function (err, isMatch) { - assert(isMatch); - User.findById(user.id, function (err, uu) { - uu.hasPassword('baz2', function (err, isMatch) { - assert(isMatch); - done(); - }); - }); - }); - }); - }); - }); -}); -``` - - -## Verification - -### user.verify(options, fn) -Verify a user's email address. - -```js -User.afterRemote('create', function(ctx, user, next) { - assert(user, 'afterRemote should include result'); - - var options = { - type: 'email', - to: user.email, - from: 'noreply@myapp.org', - redirect: '/', - protocol: ctx.req.protocol, - host: ctx.req.get('host') - }; - - user.verify(options, function (err, result) { - assert(result.email); - assert(result.email.message); - assert(result.token); - - - var lines = result.email.message.split('\n'); - assert(lines[4].indexOf('To: bar@bat.com') === 0); - done(); - }); -}); - -request(app) - .post('/users') - .expect('Content-Type', /json/) - .expect(200) - .send({email: 'bar@bat.com', password: 'bar'}) - .end(function(err, res){ - if(err) return done(err); - }); -``` - - -### User.confirm(options, fn) -Confirm a user verification. - -```js -User.afterRemote('create', function(ctx, user, next) { - assert(user, 'afterRemote should include result'); - - var options = { - type: 'email', - to: user.email, - from: 'noreply@myapp.org', - redirect: 'http://foo.com/bar', - protocol: ctx.req.protocol, - host: ctx.req.get('host') - }; - - user.verify(options, function (err, result) { - if(err) return done(err); - - request(app) - .get('/users/confirm?uid=' + result.uid + '&token=' + encodeURIComponent(result.token) + '&redirect=' + encodeURIComponent(options.redirect)) - .expect(302) - .expect('location', options.redirect) - .end(function(err, res){ - if(err) return done(err); - done(); - }); - }); -}); - -request(app) - .post('/users') - .expect('Content-Type', /json/) - .expect(302) - .send({email: 'bar@bat.com', password: 'bar'}) - .end(function(err, res){ - if(err) return done(err); - }); -``` - diff --git a/test/access-control.integration.js b/test/access-control.integration.js index c962c666..1abb320e 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -11,6 +11,7 @@ describe('access control - integration', function () { lt.beforeEach.withApp(app); + /* describe('accessToken', function() { // it('should be a sublcass of AccessToken', function () { // assert(app.models.accessToken.prototype instanceof loopback.AccessToken); @@ -54,6 +55,7 @@ describe('access control - integration', function () { return '/api/accessTokens/' + this.randomToken.id; } }); + */ describe('/users', function () { @@ -93,6 +95,10 @@ describe('access control - integration', function () { }); lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() { lt.it.shouldBeAllowed(); + it('should not include a password', function() { + var user = this.res.body; + assert.equal(user.password, undefined); + }); }); lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() { lt.it.shouldBeAllowed(); @@ -147,6 +153,7 @@ describe('access control - integration', function () { lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts'); @@ -156,12 +163,25 @@ describe('access control - integration', function () { lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount); lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { - beforeEach(function() { - this.url = '/api/accounts/' + this.user.accountId; + beforeEach(function(done) { + var self = this; + + // Create an account under the given user + app.models.account.create({ + userId: self.user.id, + balance: 100 + }, function(err, act) { + self.url = '/api/accounts/' + act.id; + done(); + }); + }); lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() { lt.it.shouldBeAllowed(); }); + lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() { + lt.it.shouldBeAllowed(); + }); lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() { lt.it.shouldBeDenied(); }); diff --git a/test/acl.test.js b/test/acl.test.js index 76449d5f..488638e7 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -70,6 +70,40 @@ describe('security scopes', function () { }); describe('security ACLs', function () { + it('should order ACL entries based on the matching score', function() { + var acls = [ + { + "model": "account", + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + }, + { + "model": "account", + "accessType": "*", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$owner" + }, + { + "model": "account", + "accessType": "READ", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$everyone" + }]; + var req = { + model: 'account', + property: 'find', + accessType: 'WRITE' + }; + var perm = ACL.resolvePermission(acls, req); + assert.deepEqual(perm, { model: 'account', + property: 'find', + accessType: 'WRITE', + permission: 'ALLOW' }); + }); it("should allow access to models for the given principal by wildcard", function () { ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, diff --git a/test/app.test.js b/test/app.test.js index 4087d33a..47f31795 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -4,12 +4,60 @@ var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); describe('app', function() { describe('app.model(Model)', function() { + var app, db; + beforeEach(function() { + app = loopback(); + db = loopback.createDataSource({connector: loopback.Memory}); + }); + it("Expose a `Model` to remote clients", function() { - var app = loopback(); - var memory = loopback.createDataSource({connector: loopback.Memory}); - var Color = memory.createModel('color', {name: String}); + var Color = db.createModel('color', {name: String}); app.model(Color); - assert.equal(app.models().length, 1); + + expect(app.models()).to.eql([Color]); + }); + + it('uses singlar name as app.remoteObjects() key', function() { + var Color = db.createModel('color', {name: String}); + app.model(Color); + expect(app.remoteObjects()).to.eql({ color: Color }); + }); + + it('uses singular name as shared class name', function() { + var Color = db.createModel('color', {name: String}); + app.model(Color); + expect(app.remotes().exports).to.eql({ color: Color }); + }); + + it('updates REST API when a new model is added', function(done) { + app.use(loopback.rest()); + request(app).get('/colors').expect(404, function(err, res) { + if (err) return done(err); + var Color = db.createModel('color', {name: String}); + app.model(Color); + request(app).get('/colors').expect(200, done); + }); + }); + + describe('in compat mode', function() { + before(function() { + loopback.compat.usePluralNamesForRemoting = true; + }); + after(function() { + loopback.compat.usePluralNamesForRemoting = false; + }); + + it('uses plural name as shared class name', function() { + var Color = db.createModel('color', {name: String}); + app.model(Color); + expect(app.remotes().exports).to.eql({ colors: Color }); + }); + + it('uses plural name as app.remoteObjects() key', function() { + var Color = db.createModel('color', {name: String}); + app.model(Color); + expect(app.remoteObjects()).to.eql({ colors: Color }); + }); }); }); @@ -36,13 +84,20 @@ describe('app', function() { }); }); + describe('app.models', function() { + it('is unique per app instance', function() { + var Color = app.model('Color', { dataSource: 'db' }); + expect(app.models.Color).to.equal(Color); + var anotherApp = loopback(); + expect(anotherApp.models.Color).to.equal(undefined); + }); + }); + describe('app.boot([options])', function () { beforeEach(function () { - var app = this.app = loopback(); - app.boot({ app: { - port: 3000, + port: 3000, host: '127.0.0.1', restApiRoot: '/rest-api', foo: {bar: 'bat'}, @@ -59,7 +114,7 @@ describe('app', function() { dataSources: { 'the-db': { connector: 'memory' - } + } } }); }); @@ -106,14 +161,14 @@ describe('app', function() { var app = loopback(); app.boot({ app: { - port: undefined, + port: undefined, host: undefined } }); return app; } }); - + it('should be honored', function() { var assertHonored = function (portKey, hostKey) { process.env[hostKey] = randomPort(); @@ -142,6 +197,12 @@ describe('app', function() { var app = this.boot(); assert.equal(app.get('host'), process.env.npm_config_host); + delete process.env.npm_config_host; + delete process.env.OPENSHIFT_SLS_IP; + delete process.env.OPENSHIFT_NODEJS_IP; + delete process.env.HOST; + delete process.env.npm_package_config_host; + process.env.npm_config_port = randomPort(); process.env.OPENSHIFT_SLS_PORT = randomPort(); process.env.OPENSHIFT_NODEJS_PORT = randomPort(); @@ -151,6 +212,12 @@ describe('app', function() { var app = this.boot(); assert.equal(app.get('host'), process.env.npm_config_host); assert.equal(app.get('port'), process.env.npm_config_port); + + delete process.env.npm_config_port; + delete process.env.OPENSHIFT_SLS_PORT; + delete process.env.OPENSHIFT_NODEJS_PORT; + delete process.env.PORT; + delete process.env.npm_package_config_port; }); function randomHost() { @@ -160,6 +227,18 @@ describe('app', function() { function randomPort() { return Math.floor(Math.random() * 10000); } + + it('should honor 0 for free port', function () { + var app = loopback(); + app.boot({app: {port: 0}}); + assert.equal(app.get('port'), 0); + }); + + it('should default to port 3000', function () { + var app = loopback(); + app.boot({app: {port: undefined}}); + assert.equal(app.get('port'), 3000); + }); }); it('Instantiate models', function () { @@ -353,6 +432,14 @@ describe('app', function() { }); }); + describe('enableAuth', function() { + it('should set app.isAuthEnabled to true', function() { + expect(app.isAuthEnabled).to.not.equal(true); + app.enableAuth(); + expect(app.isAuthEnabled).to.equal(true); + }); + }); + describe('app.get("/", loopback.status())', function () { it('should return the status of the application', function (done) { var app = loopback(); @@ -365,7 +452,8 @@ describe('app', function() { assert.equal(typeof res.body, 'object'); assert(res.body.started); - assert(res.body.uptime); + // The number can be 0 + assert(res.body.uptime !== undefined); var elapsed = Date.now() - Number(new Date(res.body.started)); diff --git a/test/e2e/remote-connector.e2e.js b/test/e2e/remote-connector.e2e.js new file mode 100644 index 00000000..3fb806fb --- /dev/null +++ b/test/e2e/remote-connector.e2e.js @@ -0,0 +1,39 @@ +var path = require('path'); +var loopback = require('../../'); +var models = require('../fixtures/e2e/models'); +var TestModel = models.TestModel; +var assert = require('assert'); + +describe('RemoteConnector', function() { + before(function() { + // setup the remote connector + var localApp = loopback(); + var ds = loopback.createDataSource({ + url: 'http://localhost:3000/api', + connector: loopback.Remote + }); + localApp.model(TestModel); + TestModel.attachTo(ds); + }); + + it('should be able to call create', function (done) { + TestModel.create({ + foo: 'bar' + }, function(err, inst) { + if(err) return done(err); + assert(inst.id); + done(); + }); + }); + + it('should be able to call save', function (done) { + var m = new TestModel({ + foo: 'bar' + }); + m.save(function(err, data) { + if(err) return done(err); + assert(m.id); + done(); + }); + }); +}); diff --git a/test/fixtures/access-control/models.json b/test/fixtures/access-control/models.json index f814353a..6db81abe 100644 --- a/test/fixtures/access-control/models.json +++ b/test/fixtures/access-control/models.json @@ -23,10 +23,6 @@ "type": "hasMany", "foreignKey": "userId" }, - "account": { - "model": "account", - "type": "belongsTo" - }, "transactions": { "model": "transaction", "type": "hasMany" @@ -103,6 +99,11 @@ "transactions": { "model": "transaction", "type": "hasMany" + }, + "user": { + "model": "user", + "type": "belongsTo", + "foreignKey": "userId" } }, "acls": [ diff --git a/test/fixtures/e2e/app.js b/test/fixtures/e2e/app.js new file mode 100644 index 00000000..337d6145 --- /dev/null +++ b/test/fixtures/e2e/app.js @@ -0,0 +1,14 @@ +var loopback = require('../../../'); +var path = require('path'); +var app = module.exports = loopback(); +var models = require('./models'); +var TestModel = models.TestModel; + +app.use(loopback.cookieParser({secret: app.get('cookieSecret')})); +var apiPath = '/api'; +app.use(apiPath, loopback.rest()); +app.use(loopback.static(path.join(__dirname, 'public'))); +app.use(loopback.urlNotFound()); +app.use(loopback.errorHandler()); +app.model(TestModel); +TestModel.attachTo(loopback.memory()); diff --git a/test/fixtures/e2e/models.js b/test/fixtures/e2e/models.js new file mode 100644 index 00000000..dad14f61 --- /dev/null +++ b/test/fixtures/e2e/models.js @@ -0,0 +1,4 @@ +var loopback = require('../../../'); +var DataModel = loopback.DataModel; + +exports.TestModel = DataModel.extend('TestModel'); diff --git a/test/fixtures/simple-integration-app/app.js b/test/fixtures/simple-integration-app/app.js index e460862a..5f71b1d3 100644 --- a/test/fixtures/simple-integration-app/app.js +++ b/test/fixtures/simple-integration-app/app.js @@ -7,7 +7,6 @@ app.use(loopback.favicon()); app.use(loopback.cookieParser({secret: app.get('cookieSecret')})); var apiPath = '/api'; app.use(apiPath, loopback.rest()); -app.use(app.router); app.use(loopback.static(path.join(__dirname, 'public'))); app.use(loopback.urlNotFound()); app.use(loopback.errorHandler()); diff --git a/test/fixtures/simple-integration-app/app.json b/test/fixtures/simple-integration-app/app.json index 0d6aed2e..c1cd9fb7 100644 --- a/test/fixtures/simple-integration-app/app.json +++ b/test/fixtures/simple-integration-app/app.json @@ -1,5 +1,14 @@ { "port": 3000, "host": "0.0.0.0", - "cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0" + "cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0", + "remoting": { + "json": { + "limit": "1kb", + "strict": false + }, + "urlencoded": { + "limit": "8kb" + } + } } \ No newline at end of file diff --git a/test/fixtures/simple-integration-app/models.json b/test/fixtures/simple-integration-app/models.json index 10a28a01..494367d7 100644 --- a/test/fixtures/simple-integration-app/models.json +++ b/test/fixtures/simple-integration-app/models.json @@ -30,7 +30,15 @@ "widget": { "properties": {}, "public": true, - "dataSource": "db" + "dataSource": "db", + "options": { + "relations": { + "store": { + "model": "store", + "type": "belongsTo" + } + } + } }, "store": { "properties": {}, diff --git a/test/geo-point.test.js b/test/geo-point.test.js index 2e436be3..2f3da597 100644 --- a/test/geo-point.test.js +++ b/test/geo-point.test.js @@ -53,4 +53,4 @@ describe('GeoPoint', function() { assert.equal(m.geo.lat, 3.444); }); }); -}); \ No newline at end of file +}); diff --git a/test/hidden-properties.test.js b/test/hidden-properties.test.js new file mode 100644 index 00000000..356c20f7 --- /dev/null +++ b/test/hidden-properties.test.js @@ -0,0 +1,59 @@ +var loopback = require('../'); + +describe('hidden properties', function () { + beforeEach(function (done) { + var app = this.app = loopback(); + var Product = this.Product = app.model('product', { + options: {hidden: ['secret']}, + dataSource: loopback.memory() + }); + var Category = this.Category = this.app.model('category', { + dataSource: loopback.memory() + }); + Category.hasMany(Product); + app.use(loopback.rest()); + Category.create({ + name: 'my category' + }, function(err, category) { + category.products.create({ + name: 'pencil', + secret: 'a secret' + }, done); + }); + }); + + afterEach(function(done) { + var Product = this.Product; + this.Category.destroyAll(function() { + Product.destroyAll(done); + }); + }) + + it('should hide a property remotely', function (done) { + request(this.app) + .get('/products') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res){ + if(err) return done(err); + var product = res.body[0]; + assert.equal(product.secret, undefined); + done(); + }); + }); + + it('should hide a property of nested models', function (done) { + var app = this.app; + request(app) + .get('/categories?filter[include]=products') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res){ + if(err) return done(err); + var category = res.body[0]; + var product = category.products[0]; + assert.equal(product.secret, undefined); + done(); + }); + }); +}); diff --git a/test/integration.test.js b/test/integration.test.js new file mode 100644 index 00000000..7aae5dc0 --- /dev/null +++ b/test/integration.test.js @@ -0,0 +1,88 @@ +var net = require('net'); +describe('loopback application', function() { + it('pauses request stream during authentication', function(done) { + // This test reproduces the issue reported in + // https://github.com/strongloop/loopback-storage-service/issues/7 + var app = loopback(); + setupAppWithStreamingMethod(); + + app.listen(0, function() { + sendHttpRequestInOnePacket( + this.address().port, + 'POST /streamers/read HTTP/1.0\n' + + 'Content-Length: 1\n' + + 'Content-Type: application/x-custom-octet-stream\n' + + '\n' + + 'X', + function(err, res) { + if (err) return done(err); + expect(res).to.match(/\nX$/); + done(); + }); + }); + + function setupAppWithStreamingMethod() { + app.dataSource('db', { + connector: loopback.Memory, + defaultForType: 'db' + }); + var db = app.datasources.db; + + loopback.User.attachTo(db); + loopback.AccessToken.attachTo(db); + loopback.Role.attachTo(db); + loopback.ACL.attachTo(db); + loopback.User.hasMany(loopback.AccessToken, { as: 'accessTokens' }); + + var Streamer = app.model('Streamer', { dataSource: 'db' }); + Streamer.read = function(req, res, cb) { + var body = new Buffer(0); + req.on('data', function(chunk) { + body += chunk; + }); + req.on('end', function() { + res.end(body.toString()); + // we must not call the callback here + // because it will attempt to add response headers + }); + req.once('error', function(err) { + cb(err); + }); + }; + loopback.remoteMethod(Streamer.read, { + http: { method: 'post' }, + accepts: [ + { arg: 'req', type: 'Object', http: { source: 'req' } }, + { arg: 'res', type: 'Object', http: { source: 'res' } } + ] + }); + + app.enableAuth(); + app.use(loopback.token({ model: app.models.accessToken })); + app.use(loopback.rest()); + } + + function sendHttpRequestInOnePacket(port, reqString, cb) { + var socket = net.createConnection(port); + var response = new Buffer(0); + + socket.on('data', function(chunk) { + response += chunk; + }); + socket.on('end', function() { + callCb(null, response.toString()); + }); + socket.once('error', function(err) { + callCb(err); + }); + + socket.write(reqString.replace(/\n/g, '\r\n')); + + function callCb(err, res) { + if (!cb) return; + cb(err, res); + cb = null; + } + } + }); +}); diff --git a/test/model.application.test.js b/test/model.application.test.js index d27a4750..5d8a92a5 100644 --- a/test/model.application.test.js +++ b/test/model.application.test.js @@ -20,6 +20,7 @@ describe('Application', function () { assert(app.masterKey); assert(app.created); assert(app.modified); + assert.equal(typeof app.id, 'string'); done(err, result); }); }); @@ -121,7 +122,8 @@ describe('Application', function () { it('Authenticate with application id & clientKey', function (done) { Application.authenticate(registeredApp.id, registeredApp.clientKey, function (err, result) { - assert.equal(result, 'clientKey'); + assert.equal(result.application.id, registeredApp.id); + assert.equal(result.keyType, 'clientKey'); done(err, result); }); }); @@ -129,7 +131,8 @@ describe('Application', function () { it('Authenticate with application id & javaScriptKey', function (done) { Application.authenticate(registeredApp.id, registeredApp.javaScriptKey, function (err, result) { - assert.equal(result, 'javaScriptKey'); + assert.equal(result.application.id, registeredApp.id); + assert.equal(result.keyType, 'javaScriptKey'); done(err, result); }); }); @@ -137,7 +140,8 @@ describe('Application', function () { it('Authenticate with application id & restApiKey', function (done) { Application.authenticate(registeredApp.id, registeredApp.restApiKey, function (err, result) { - assert.equal(result, 'restApiKey'); + assert.equal(result.application.id, registeredApp.id); + assert.equal(result.keyType, 'restApiKey'); done(err, result); }); }); @@ -145,7 +149,8 @@ describe('Application', function () { it('Authenticate with application id & masterKey', function (done) { Application.authenticate(registeredApp.id, registeredApp.masterKey, function (err, result) { - assert.equal(result, 'masterKey'); + assert.equal(result.application.id, registeredApp.id); + assert.equal(result.keyType, 'masterKey'); done(err, result); }); }); @@ -153,7 +158,8 @@ describe('Application', function () { it('Authenticate with application id & windowsKey', function (done) { Application.authenticate(registeredApp.id, registeredApp.windowsKey, function (err, result) { - assert.equal(result, 'windowsKey'); + assert.equal(result.application.id, registeredApp.id); + assert.equal(result.keyType, 'windowsKey'); done(err, result); }); }); @@ -170,13 +176,14 @@ describe('Application', function () { describe('Application subclass', function () { it('should use subclass model name', function (done) { var MyApp = Application.extend('MyApp'); - MyApp.attachTo(loopback.createDataSource({connector: loopback.Memory})); - MyApp.register('rfeng', 'MyApp2', - {description: 'My second mobile application'}, function (err, result) { + var ds = loopback.createDataSource({connector: loopback.Memory}); + MyApp.attachTo(ds); + MyApp.register('rfeng', 'MyApp123', + {description: 'My 123 mobile application'}, function (err, result) { var app = result; assert.equal(app.owner, 'rfeng'); - assert.equal(app.name, 'MyApp2'); - assert.equal(app.description, 'My second mobile application'); + assert.equal(app.name, 'MyApp123'); + assert.equal(app.description, 'My 123 mobile application'); assert(app.clientKey); assert(app.javaScriptKey); assert(app.restApiKey); @@ -184,14 +191,17 @@ describe('Application subclass', function () { assert(app.masterKey); assert(app.created); assert(app.modified); - MyApp.findById(app.id, function (err, myApp) { - assert(!err); - assert(myApp); - - Application.findById(app.id, function (err, myApp) { + // Remove all instances from Application model to avoid left-over data + Application.destroyAll(function () { + MyApp.findById(app.id, function (err, myApp) { assert(!err); - assert(myApp === null); - done(err, myApp); + assert(myApp); + + Application.findById(app.id, function (err, myApp) { + assert(!err); + assert(myApp === null); + done(err, myApp); + }); }); }); }); diff --git a/test/model.test.js b/test/model.test.js index ca8e4999..8eaf7675 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1,4 +1,5 @@ var async = require('async'); +require('./support'); var loopback = require('../'); var ACL = loopback.ACL; var Change = loopback.Change; @@ -197,21 +198,21 @@ describe('Model', function() { }); }); - describe('Model.deleteById([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(!err); - assert.equal(notFound, null); - done(); - }); - }); - }); - }); - }); + describe('Model.deleteById([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(!err); + assert.equal(notFound, null); + done(); + }); + }); + }); + }); + }); - describe('Model.destroyAll(callback)', function() { + describe('Model.destroyAll(callback)', function() { it("Delete all Model instances from data source", function(done) { (new TaskEmitter()) .task(User, 'create', {first: 'jill'}) @@ -263,7 +264,8 @@ describe('Model', function() { }); }); - describe('Remote Methods', function(){ + describe.onServer('Remote Methods', function(){ + beforeEach(function () { User.login = function (username, password, fn) { if(username === 'foo' && password === 'bar') { @@ -429,6 +431,37 @@ describe('Model', function() { }); }); }) + + describe('in compat mode', function() { + before(function() { + loopback.compat.usePluralNamesForRemoting = true; + }); + after(function() { + loopback.compat.usePluralNamesForRemoting = false; + }); + + it('correctly install before/after hooks', function(done) { + var hooksCalled = []; + + User.beforeRemote('**', function(ctx, user, next) { + hooksCalled.push('beforeRemote'); + next(); + }); + + User.afterRemote('**', function(ctx, user, next) { + hooksCalled.push('afterRemote'); + next(); + }); + + request(app).get('/users') + .expect(200, function(err, res) { + if (err) return done(err); + expect(hooksCalled, 'hooks called') + .to.eql(['beforeRemote', 'afterRemote']); + done(); + }); + }); + }); }); describe('Model.hasMany(Model)', function() { @@ -705,4 +738,14 @@ describe('Model', function() { }); }); }); + + describe('Model._getACLModel()', function() { + it('should return the subclass of ACL', function() { + var Model = require('../').Model; + var acl = ACL.extend('acl'); + Model._ACL(null); // Reset the ACL class for the base model + var model = Model._ACL(); + assert.equal(model, acl); + }); + }); }); diff --git a/test/relations.integration.js b/test/relations.integration.js index 2726b306..0c3df8df 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -4,6 +4,7 @@ var path = require('path'); var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app'); var app = require(path.join(SIMPLE_APP, 'app.js')); var assert = require('assert'); +var expect = require('chai').expect; describe('relations - integration', function () { @@ -95,4 +96,132 @@ describe('relations - integration', function () { }); }); + describe('/widgets/:id/store', function () { + beforeEach(function (done) { + var self = this; + this.store.widgets.create({ + name: this.widgetName + }, function(err, widget) { + self.widget = widget; + self.url = '/api/widgets/' + self.widget.id + '/store'; + done(); + }); + }); + lt.describe.whenCalledRemotely('GET', '/api/widgets/:id/store', function () { + it('should succeed with statusCode 200', function () { + assert.equal(this.res.statusCode, 200); + assert.equal(this.res.body.id, this.store.id); + }); + }); + }); + + describe('hasAndBelongsToMany', function() { + beforeEach(function defineProductAndCategoryModels() { + var product = app.model( + 'product', + { properties: { id: 'string', name: 'string' }, dataSource: 'db' } + + ); + var category = app.model( + 'category', + { properties: { id: 'string', name: 'string' }, dataSource: 'db' } + ); + product.hasAndBelongsToMany(category); + category.hasAndBelongsToMany(product); + }); + + lt.beforeEach.givenModel('category'); + + beforeEach(function createProductsInCategory(done) { + var test = this; + this.category.products.create({ + name: 'a-product' + }, function(err, product) { + if (err) return done(err); + test.product = product; + done(); + }); + }); + + beforeEach(function createAnotherCategoryAndProduct(done) { + app.models.category.create({ name: 'another-category' }, + function(err, cat) { + if (err) return done(err); + cat.products.create({ name: 'another-product' }, done); + }); + }); + + afterEach(function(done) { + this.app.models.product.destroyAll(done); + }); + + it.skip('allows to find related objects via where filter', function(done) { + //TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94 + var expectedProduct = this.product; + // Note: the URL format is not final + this.get('/api/products?filter[where][categoryId]=' + this.category.id) + .expect(200, function(err, res) { + if (err) return done(err); + expect(res.body).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + + it('allows to find related object via URL scope', function(done) { + var expectedProduct = this.product; + this.get('/api/categories/' + this.category.id + '/products') + .expect(200, function(err, res) { + if (err) return done(err); + expect(res.body).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + + it('includes requested related models in `find`', function(done) { + var expectedProduct = this.product; + var url = '/api/categories/findOne?filter[where][id]=' + + this.category.id + '&filter[include]=products'; + + this.get(url) + .expect(200, function(err, res) { + expect(res.body).to.have.property('products'); + expect(res.body.products).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + + it.skip('includes requested related models in `findById`', function(done) { + //TODO https://github.com/strongloop/loopback-datasource-juggler/issues/93 + var expectedProduct = this.product; + // Note: the URL format is not final + var url = '/api/categories/' + this.category.id + '?include=products'; + + this.get(url) + .expect(200, function(err, res) { + expect(res.body).to.have.property('products'); + expect(res.body.products).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name + } + ]); + done(); + }); + }); + }); }); diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js new file mode 100644 index 00000000..2a55c02c --- /dev/null +++ b/test/remote-connector.test.js @@ -0,0 +1,43 @@ +var loopback = require('../'); + +describe('RemoteConnector', function() { + beforeEach(function(done) { + var LocalModel = this.LocalModel = loopback.DataModel.extend('LocalModel'); + var RemoteModel = loopback.DataModel.extend('LocalModel'); + var localApp = loopback(); + var remoteApp = loopback(); + localApp.model(LocalModel); + remoteApp.model(RemoteModel); + remoteApp.use(loopback.rest()); + RemoteModel.attachTo(loopback.memory()); + remoteApp.listen(0, function() { + var ds = loopback.createDataSource({ + host: remoteApp.get('host'), + port: remoteApp.get('port'), + connector: loopback.Remote + }); + + LocalModel.attachTo(ds); + done(); + }); + }); + + it('should alow methods to be called remotely', function (done) { + var data = {foo: 'bar'}; + this.LocalModel.create(data, function(err, result) { + if(err) return done(err); + expect(result).to.deep.equal({id: 1, foo: 'bar'}); + done(); + }); + }); + + it('should alow instance methods to be called remotely', function (done) { + var data = {foo: 'bar'}; + var m = new this.LocalModel(data); + m.save(function(err, result) { + if(err) return done(err); + expect(result).to.deep.equal({id: 2, foo: 'bar'}); + done(); + }); + }); +}); diff --git a/test/remoting.integration.js b/test/remoting.integration.js new file mode 100644 index 00000000..8fa987f2 --- /dev/null +++ b/test/remoting.integration.js @@ -0,0 +1,69 @@ +var loopback = require('../'); +var lt = require('loopback-testing'); +var path = require('path'); +var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app'); +var app = require(path.join(SIMPLE_APP, 'app.js')); +var assert = require('assert'); + +describe('remoting - integration', function () { + + lt.beforeEach.withApp(app); + lt.beforeEach.givenModel('store'); + + afterEach(function (done) { + this.app.models.store.destroyAll(done); + }); + + describe('app.remotes.options', function () { + it("should load remoting options", function () { + var remotes = app.remotes(); + assert.deepEqual(remotes.options, {"json": {"limit": "1kb", "strict": false}, + "urlencoded": {"limit": "8kb"}}); + }); + + it("rest handler", function () { + var handler = app.handler('rest'); + assert(handler); + }); + + it('should accept request that has entity below 1kb', function (done) { + // Build an object that is smaller than 1kb + var name = ""; + for (var i = 0; i < 256; i++) { + name += "11"; + } + this.http = this.post('/api/stores'); + this.http.send({ + "name": name + }); + this.http.end(function (err) { + if (err) return done(err); + this.req = this.http.req; + this.res = this.http.res; + assert.equal(this.res.statusCode, 200); + done(); + }.bind(this)); + }); + + it('should reject request that has entity beyond 1kb', function (done) { + // Build an object that is larger than 1kb + var name = ""; + for (var i = 0; i < 2048; i++) { + name += "11111111111"; + } + this.http = this.post('/api/stores'); + this.http.send({ + "name": name + }); + this.http.end(function (err) { + if (err) return done(err); + this.req = this.http.req; + this.res = this.http.res; + // Request is rejected with 413 + assert.equal(this.res.statusCode, 413); + done(); + }.bind(this)); + }); + }); + +}); diff --git a/test/role.test.js b/test/role.test.js index 163e5610..3c43bc9e 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -16,6 +16,8 @@ describe('role model', function () { beforeEach(function() { ds = loopback.createDataSource({connector: 'memory'}); + // Re-attach the models so that they can have isolated store to avoid + // pollutions from other tests User.attachTo(ds); Role.attachTo(ds); RoleMapping.attachTo(ds); diff --git a/test/support.js b/test/support.js index 394bc6d4..6737119a 100644 --- a/test/support.js +++ b/test/support.js @@ -11,13 +11,12 @@ app = null; TaskEmitter = require('strong-task-emitter'); request = require('supertest'); - // Speed up the password hashing algorithm // for tests using the built-in User model loopback.User.settings.saltWorkFactor = 4; beforeEach(function () { - app = loopback(); + this.app = app = loopback(); // setup default data sources loopback.setDefaultDataSourceForType('db', { @@ -50,3 +49,19 @@ assert.isFunc = function (obj, name) { assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist'); assert(typeof obj[name] === 'function', name + ' is not a function'); } + +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); + } +}; \ No newline at end of file diff --git a/test/user.test.js b/test/user.test.js index 20271335..7f5cedbb 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -4,20 +4,25 @@ var passport = require('passport'); var MailConnector = require('../lib/connectors/mail'); var userMemory = loopback.createDataSource({ - connector: loopback.Memory + connector: 'memory' }); describe('User', function(){ + var validCredentials = {email: 'foo@bar.com', password: 'bar'}; + var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'}; + var incompleteCredentials = {password: 'bar1'}; + beforeEach(function() { User = loopback.User.extend('user'); User.email = loopback.Email.extend('email'); loopback.autoAttach(); + + // Update the AccessToken relation to use the subclass of User + AccessToken.belongsTo(User); // allow many User.afterRemote's to be called User.setMaxListeners(0); - - User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'}); - AccessToken.belongsTo(User); + }); beforeEach(function (done) { @@ -25,7 +30,7 @@ describe('User', function(){ app.use(loopback.rest()); app.model(User); - User.create({email: 'foo@bar.com', password: 'bar'}, done); + User.create(validCredentials, done); }); afterEach(function (done) { @@ -105,7 +110,7 @@ describe('User', function(){ describe('User.login', function() { it('Login a user by providing credentials', function(done) { - User.login({email: 'foo@bar.com', password: 'bar'}, function (err, accessToken) { + User.login(validCredentials, function (err, accessToken) { assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); @@ -119,7 +124,7 @@ describe('User', function(){ .post('/users/login') .expect('Content-Type', /json/) .expect(200) - .send({email: 'foo@bar.com', password: 'bar'}) + .send(validCredentials) .end(function(err, res){ if(err) return done(err); var accessToken = res.body; @@ -127,11 +132,62 @@ describe('User', function(){ assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); + assert(accessToken.user === undefined); done(); }); }); - + + it('Login a user over REST by providing invalid credentials', function(done) { + request(app) + .post('/users/login') + .expect('Content-Type', /json/) + .expect(401) + .send(invalidCredentials) + .end(function(err, res){ + done(); + }); + }); + + it('Login a user over REST by providing incomplete credentials', function(done) { + request(app) + .post('/users/login') + .expect('Content-Type', /json/) + .expect(400) + .send(incompleteCredentials) + .end(function(err, res){ + done(); + }); + }); + + it('Login a user over REST with the wrong Content-Type', function(done) { + request(app) + .post('/users/login') + .set('Content-Type', null) + .expect('Content-Type', /json/) + .expect(400) + .send(validCredentials) + .end(function(err, res){ + done(); + }); + }); + + it('Returns current user when `include` is `USER`', function(done) { + request(app) + .post('/users/login?include=USER') + .send(validCredentials) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + var token = res.body; + expect(token.user, 'body.user').to.not.equal(undefined); + expect(token.user, 'body.user') + .to.have.property('email', validCredentials.email); + done(); + }); + }); + it('Login should only allow correct credentials', function(done) { User.create({email: 'foo22@bar.com', password: 'bar'}, function(user, err) { User.login({email: 'foo44@bar.com', password: 'bar'}, function(err, accessToken) {