Merge branch master
This commit is contained in:
commit
e86a00de69
|
@ -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']);
|
||||||
|
|
||||||
|
};
|
294
LICENSE
294
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
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.
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# LoopBack
|
# LoopBack
|
||||||
|
|
||||||
|
For a quick introduction and overview, see http://loopback.io/.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
[See the full documentation](http://docs.strongloop.com/display/DOC/LoopBack).
|
[See the full documentation](http://docs.strongloop.com/display/DOC/LoopBack).
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"lib/models/application.js",
|
"lib/models/application.js",
|
||||||
"lib/models/email.js",
|
"lib/models/email.js",
|
||||||
"lib/models/model.js",
|
"lib/models/model.js",
|
||||||
|
"lib/models/data-model.js",
|
||||||
"lib/models/role.js",
|
"lib/models/role.js",
|
||||||
"lib/models/user.js",
|
"lib/models/user.js",
|
||||||
"lib/models/change.js",
|
"lib/models/change.js",
|
||||||
|
|
|
@ -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.
|
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
|
#### ctx.result
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ var oracle = loopback.createDataSource({
|
||||||
User.attachTo(oracle);
|
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
|
### Properties
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ User.findById(23, function(err, user) {
|
||||||
Find a single instance that matches the given where expression.
|
Find a single instance that matches the given where expression.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
User.findOne({id: 23}, function(err, user) {
|
User.findOne({where: {id: 23}}, function(err, user) {
|
||||||
console.info(user.id); // 23
|
console.info(user.id); // 23
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
1
index.js
1
index.js
|
@ -12,6 +12,7 @@ var datasourceJuggler = require('loopback-datasource-juggler');
|
||||||
loopback.Connector = require('./lib/connectors/base-connector');
|
loopback.Connector = require('./lib/connectors/base-connector');
|
||||||
loopback.Memory = require('./lib/connectors/memory');
|
loopback.Memory = require('./lib/connectors/memory');
|
||||||
loopback.Mail = require('./lib/connectors/mail');
|
loopback.Mail = require('./lib/connectors/mail');
|
||||||
|
loopback.Remote = require('./lib/connectors/remote');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
var DataSource = require('loopback-datasource-juggler').DataSource
|
var DataSource = require('loopback-datasource-juggler').DataSource
|
||||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
||||||
|
, compat = require('./compat')
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, fs = require('fs')
|
, fs = require('fs')
|
||||||
|
, _ = require('underscore')
|
||||||
, RemoteObjects = require('strong-remoting')
|
, RemoteObjects = require('strong-remoting')
|
||||||
, swagger = require('strong-remoting/ext/swagger')
|
, swagger = require('strong-remoting/ext/swagger')
|
||||||
, stringUtils = require('underscore.string')
|
, 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).
|
* 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
|
* **NOTE:** Calling `app.remotes()` more than once returns only a single set of remote objects.
|
||||||
* single set of remote objects.
|
|
||||||
* @returns {RemoteObjects}
|
* @returns {RemoteObjects}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -55,7 +56,13 @@ app.remotes = function () {
|
||||||
if(this._remotes) {
|
if(this._remotes) {
|
||||||
return this._remotes;
|
return this._remotes;
|
||||||
} else {
|
} 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
|
* @param {String} modelName The name of the model to define.
|
||||||
* @options {Object} config The model's configuration
|
* @options {Object} config The model's configuration.
|
||||||
* @property {String} dataSource The `DataSource` to attach the model to
|
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
|
||||||
* @property {Object} [options] an object containing `Model` options
|
* @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)
|
* @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
|
* @end
|
||||||
* @returns {ModelConstructor} the model class
|
* @returns {ModelConstructor} the model class
|
||||||
*/
|
*/
|
||||||
|
@ -97,9 +106,11 @@ app.disuse = function (route) {
|
||||||
app.model = function (Model, config) {
|
app.model = function (Model, config) {
|
||||||
if(arguments.length === 1) {
|
if(arguments.length === 1) {
|
||||||
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
|
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
|
||||||
assert(Model.pluralModelName, 'Model must have a "pluralModelName" property');
|
assert(Model.modelName, 'Model must have a "modelName" property');
|
||||||
this.remotes().exports[Model.pluralModelName] = Model;
|
var remotingClassName = compat.getClassNameForRemoting(Model);
|
||||||
|
this.remotes().exports[remotingClassName] = Model;
|
||||||
this.models().push(Model);
|
this.models().push(Model);
|
||||||
|
clearHandlerCache(this);
|
||||||
Model.shared = true;
|
Model.shared = true;
|
||||||
Model.app = this;
|
Model.app = this;
|
||||||
Model.emit('attached', 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()`
|
* Get the models exported by the app. Returns only models defined using `app.model()`
|
||||||
* will show up in this list.
|
|
||||||
*
|
*
|
||||||
* There are two ways how to access models.
|
* There are two ways to access models:
|
||||||
*
|
*
|
||||||
* **1. A list of all models**
|
* 1. Call `app.models()` to get a list of all models.
|
||||||
*
|
|
||||||
* Call `app.models()` to get a list of all models.
|
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* var models = app.models();
|
* 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.
|
* `app.model` has properties for all defined models.
|
||||||
*
|
*
|
||||||
* In the following example the `Product` and `CustomerReceipt` models are
|
* The following example illustrates accessing the `Product` and `CustomerReceipt` models
|
||||||
* accessed using the `models` object.
|
* using the `models` object.
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* var loopback = require('loopback');
|
* var loopback = require('loopback');
|
||||||
|
@ -171,7 +178,7 @@ app.model = function (Model, config) {
|
||||||
* var customerReceipt = app.models.customerReceipt;
|
* var customerReceipt = app.models.customerReceipt;
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @returns {Array} a list of model classes
|
* @returns {Array} Array of model classes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.models = function () {
|
app.models = function () {
|
||||||
|
@ -193,6 +200,7 @@ app.dataSource = function (name, config) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all remote objects.
|
* Get all remote objects.
|
||||||
|
* @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.remoteObjects = function () {
|
app.remoteObjects = function () {
|
||||||
|
@ -203,26 +211,17 @@ app.remoteObjects = function () {
|
||||||
models.forEach(function (ModelCtor) {
|
models.forEach(function (ModelCtor) {
|
||||||
// only add shared models
|
// only add shared models
|
||||||
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
|
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
|
||||||
result[ModelCtor.pluralModelName] = ModelCtor;
|
result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the apps set of remote objects.
|
|
||||||
*/
|
|
||||||
|
|
||||||
app.remotes = function () {
|
|
||||||
return this._remotes || (this._remotes = RemoteObjects.create());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable swagger REST API documentation.
|
* Enable swagger REST API documentation.
|
||||||
*
|
*
|
||||||
* > Note: This method is deprecated, use the extension
|
* **Note**: This method is deprecated. Use [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
|
||||||
* [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
|
|
||||||
*
|
*
|
||||||
* **Options**
|
* **Options**
|
||||||
*
|
*
|
||||||
|
@ -282,11 +281,16 @@ app.enableAuth = function() {
|
||||||
var modelId = modelInstance && modelInstance.id || req.param('id');
|
var modelId = modelInstance && modelInstance.id || req.param('id');
|
||||||
|
|
||||||
if(Model.checkAccess) {
|
if(Model.checkAccess) {
|
||||||
|
// Pause the request before checking access
|
||||||
|
// See https://github.com/strongloop/loopback-storage-service/issues/7
|
||||||
|
req.pause();
|
||||||
Model.checkAccess(
|
Model.checkAccess(
|
||||||
req.accessToken,
|
req.accessToken,
|
||||||
modelId,
|
modelId,
|
||||||
method.name,
|
method.name,
|
||||||
function(err, allowed) {
|
function(err, allowed) {
|
||||||
|
// Emit any cached data events that fired while checking access.
|
||||||
|
req.resume();
|
||||||
if(err) {
|
if(err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
next(err);
|
next(err);
|
||||||
|
@ -303,34 +307,37 @@ app.enableAuth = function() {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
this.isAuthEnabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize an application from an options object or a set of JSON and JavaScript files.
|
* 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
|
* If the argument is a string, then it sets the application root directory based on the string value. Then it:
|
||||||
* 2. **Models** are created from an `options.models` object or `models.json` in the current directory
|
* 1. Creates DataSources from the `datasources.json` file in the application root directory.
|
||||||
* 3. Any JavaScript files in the `./models` directory are loaded with `require()`.
|
* 2. Creates Models from the `models.json` file in the application root directory.
|
||||||
* 4. Any JavaScript files in the `./boot` directory are loaded with `require()`.
|
|
||||||
*
|
*
|
||||||
* **Options**
|
* 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.
|
||||||
* - `cwd` - _optional_ - the directory to use when loading JSON and JavaScript files
|
* Then it:
|
||||||
* - `models` - _optional_ - an object containing `Model` definitions
|
* 1. Creates DataSources from the `options.dataSources` object.
|
||||||
* - `dataSources` - _optional_ - an object containing `DataSource` definitions
|
* 2. Creates Models from the `options.models` object.
|
||||||
*
|
*
|
||||||
* > **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
|
* In both cases, the function loads JavaScript files in the `/models` and `/boot` subdirectories of the application root directory with `require()`.
|
||||||
* > files may result
|
*
|
||||||
* > in models being **undefined** due to race conditions. To avoid this when
|
* **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
|
||||||
* > using `app.boot()`
|
* files may result in models being **undefined** due to race conditions.
|
||||||
* > make sure all models are passed as part of the `models` definition.
|
* 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.
|
||||||
*
|
*
|
||||||
* <a name="model-definition"></a>
|
* <a name="model-definition"></a>
|
||||||
* **Model Definitions**
|
* **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
|
* ```js
|
||||||
* {
|
* {
|
||||||
|
@ -375,20 +382,13 @@ app.enableAuth = function() {
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
* @options {String|Object} options Boot options; If String, this is the application root directory; if object, has below properties.
|
||||||
* **Model definition 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).
|
||||||
* - `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to
|
* @property {Object} dataSources Object containing `DataSource` definitions (optional).
|
||||||
* - `options` - _optional_ - an object containing `Model` options
|
* @end
|
||||||
* - `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)
|
|
||||||
*
|
*
|
||||||
* @header app.boot([options])
|
* @header app.boot([options])
|
||||||
* @throws {Error} If config is not valid
|
|
||||||
* @throws {Error} If boot fails
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.boot = function(options) {
|
app.boot = function(options) {
|
||||||
|
@ -427,15 +427,16 @@ app.boot = function(options) {
|
||||||
process.env.npm_package_config_host ||
|
process.env.npm_package_config_host ||
|
||||||
app.get('host');
|
app.get('host');
|
||||||
|
|
||||||
appConfig.port =
|
appConfig.port = _.find([
|
||||||
process.env.npm_config_port ||
|
process.env.npm_config_port,
|
||||||
process.env.OPENSHIFT_SLS_PORT ||
|
process.env.OPENSHIFT_SLS_PORT,
|
||||||
process.env.OPENSHIFT_NODEJS_PORT ||
|
process.env.OPENSHIFT_NODEJS_PORT,
|
||||||
process.env.PORT ||
|
process.env.PORT,
|
||||||
appConfig.port ||
|
appConfig.port,
|
||||||
process.env.npm_package_config_port ||
|
process.env.npm_package_config_port,
|
||||||
app.get('port') ||
|
app.get('port'),
|
||||||
3000;
|
3000
|
||||||
|
], _.isFinite);
|
||||||
|
|
||||||
appConfig.restApiRoot =
|
appConfig.restApiRoot =
|
||||||
appConfig.restApiRoot ||
|
appConfig.restApiRoot ||
|
||||||
|
@ -540,7 +541,11 @@ function dataSourcesFromConfig(config) {
|
||||||
|
|
||||||
function modelFromConfig(name, config, app) {
|
function modelFromConfig(name, config, app) {
|
||||||
var ModelCtor = require('./loopback').createModel(name, config.properties, config.options);
|
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 +'"');
|
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.
|
* Install all express middleware required by LoopBack.
|
||||||
*
|
*
|
||||||
* It is possible to inject your own middleware by listening on one of the
|
* 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
|
* This way the port param contains always the real port number, even when
|
||||||
* listen was called with port number 0.
|
* 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.
|
* for the server's "listening" event.
|
||||||
* @returns {http.Server} A node `http.Server` with this application configured
|
* @returns {http.Server} A node `http.Server` with this application configured
|
||||||
* as the request handler.
|
* as the request handler.
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = browserExpress;
|
||||||
|
|
||||||
|
function browserExpress() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
browserExpress.errorHandler = {};
|
|
@ -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;
|
||||||
|
};
|
|
@ -5,6 +5,7 @@
|
||||||
var mailer = require('nodemailer')
|
var mailer = require('nodemailer')
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, debug = require('debug')
|
, debug = require('debug')
|
||||||
|
, loopback = require('../loopback')
|
||||||
, STUB = 'STUB';
|
, STUB = 'STUB';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,8 +23,10 @@ function MailConnector(settings) {
|
||||||
var transports = settings.transports || [];
|
var transports = settings.transports || [];
|
||||||
this.transportsIndex = {};
|
this.transportsIndex = {};
|
||||||
this.transports = [];
|
this.transports = [];
|
||||||
|
|
||||||
transports.forEach(this.setupTransport.bind(this));
|
if(loopback.isServer) {
|
||||||
|
transports.forEach(this.setupTransport.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MailConnector.initialize = function(dataSource, callback) {
|
MailConnector.initialize = function(dataSource, callback) {
|
||||||
|
|
|
@ -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() {}
|
|
@ -8,26 +8,40 @@ var express = require('express')
|
||||||
, EventEmitter = require('events').EventEmitter
|
, EventEmitter = require('events').EventEmitter
|
||||||
, path = require('path')
|
, path = require('path')
|
||||||
, proto = require('./application')
|
, proto = require('./application')
|
||||||
, utils = require('express/node_modules/connect').utils
|
|
||||||
, DataSource = require('loopback-datasource-juggler').DataSource
|
, DataSource = require('loopback-datasource-juggler').DataSource
|
||||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
, 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
|
* methods to create models and data sources. The module itself is a function
|
||||||
* that creates loopback `app`. For example,
|
* that creates loopback `app`. For example,
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* ```js
|
* ```js
|
||||||
* var loopback = require('loopback');
|
* var loopback = require('loopback');
|
||||||
* var app = loopback();
|
* var app = loopback();
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* @class loopback
|
||||||
|
* @header loopback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loopback = exports = module.exports = createApplication;
|
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.
|
* Framework version.
|
||||||
*/
|
*/
|
||||||
|
@ -40,7 +54,12 @@ loopback.version = require('../package.json').version;
|
||||||
|
|
||||||
loopback.mime = express.mime;
|
loopback.mime = express.mime;
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
|
* Compatibility layer, intentionally left undocumented.
|
||||||
|
*/
|
||||||
|
loopback.compat = require('./compat');
|
||||||
|
|
||||||
|
/*!
|
||||||
* Create an loopback application.
|
* Create an loopback application.
|
||||||
*
|
*
|
||||||
* @return {Function}
|
* @return {Function}
|
||||||
|
@ -50,7 +69,12 @@ loopback.mime = express.mime;
|
||||||
function createApplication() {
|
function createApplication() {
|
||||||
var app = express();
|
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;
|
return app;
|
||||||
}
|
}
|
||||||
|
@ -70,16 +94,20 @@ for (var key in express) {
|
||||||
/*!
|
/*!
|
||||||
* Expose additional loopback middleware
|
* Expose additional loopback middleware
|
||||||
* for example `loopback.configure` etc.
|
* for example `loopback.configure` etc.
|
||||||
|
*
|
||||||
|
* ***only in node***
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fs
|
if (loopback.isServer) {
|
||||||
.readdirSync(path.join(__dirname, 'middleware'))
|
fs
|
||||||
.filter(function (file) {
|
.readdirSync(path.join(__dirname, 'middleware'))
|
||||||
return file.match(/\.js$/);
|
.filter(function (file) {
|
||||||
})
|
return file.match(/\.js$/);
|
||||||
.forEach(function (m) {
|
})
|
||||||
loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m);
|
.forEach(function (m) {
|
||||||
});
|
loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Error handler title
|
* Error handler title
|
||||||
|
@ -90,11 +118,10 @@ loopback.errorHandler.title = 'Loopback';
|
||||||
/**
|
/**
|
||||||
* Create a data source with passing the provided options to the connector.
|
* Create a data source with passing the provided options to the connector.
|
||||||
*
|
*
|
||||||
* @param {String} name (optional)
|
* @param {String} name Optional name.
|
||||||
* @param {Object} options
|
* @options {Object} Data Source options
|
||||||
*
|
* @property {Object} connector LoopBack connector.
|
||||||
* - connector - an loopback connector
|
* @property {*} Other properties See the relevant connector documentation.
|
||||||
* - other values - see the specified `connector` docs
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
loopback.createDataSource = function (name, options) {
|
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.
|
* 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} properties
|
||||||
* @param {Object} options (optional)
|
* @param {Object} options (optional)
|
||||||
*/
|
*/
|
||||||
|
@ -138,7 +165,7 @@ loopback.createModel = function (name, properties, options) {
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a remote method to a model.
|
* Add a remote method to a model.
|
||||||
|
@ -154,7 +181,7 @@ loopback.remoteMethod = function (fn, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
fn.http = fn.http || {verb: 'get'};
|
fn.http = fn.http || {verb: 'get'};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a template helper.
|
* Create a template helper.
|
||||||
|
@ -170,7 +197,7 @@ loopback.template = function (file) {
|
||||||
var templates = this._templates || (this._templates = {});
|
var templates = this._templates || (this._templates = {});
|
||||||
var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
|
var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
|
||||||
return ejs.compile(str);
|
return ejs.compile(str);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an in-memory data source. Use one if it already exists.
|
* Get an in-memory data source. Use one if it already exists.
|
||||||
|
@ -192,12 +219,12 @@ loopback.memory = function (name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return memory;
|
return memory;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up a model class by name from all models created by loopback.createModel()
|
* Look up a model class by name from all models created by loopback.createModel()
|
||||||
* @param {String} modelName The model name
|
* @param {String} modelName The model name
|
||||||
* @return {Model} The model class
|
* @returns {Model} The model class
|
||||||
*/
|
*/
|
||||||
loopback.getModel = function(modelName) {
|
loopback.getModel = function(modelName) {
|
||||||
return loopback.Model.modelBuilder.models[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
|
* 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.
|
* to find configured models in models.json over the base model.
|
||||||
* @param {Model} The base model class
|
* @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) {
|
loopback.getModelByType = function(modelType) {
|
||||||
assert(typeof modelType === 'function', 'The model type must be a constructor');
|
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`.
|
* Set the default `dataSource` for a given `type`.
|
||||||
* @param {String} type The datasource type
|
* @param {String} type The datasource type
|
||||||
* @param {Object|DataSource} dataSource The data source settings or instance
|
* @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) {
|
loopback.setDefaultDataSourceForType = function(type, dataSource) {
|
||||||
|
@ -236,17 +263,17 @@ loopback.setDefaultDataSourceForType = function(type, dataSource) {
|
||||||
|
|
||||||
defaultDataSources[type] = dataSource;
|
defaultDataSources[type] = dataSource;
|
||||||
return dataSource;
|
return dataSource;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default `dataSource` for a given `type`.
|
* Get the default `dataSource` for a given `type`.
|
||||||
* @param {String} type The datasource type
|
* @param {String} type The datasource type
|
||||||
* @return {DataSource} The data source instance
|
* @returns {DataSource} The data source instance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
loopback.getDefaultDataSourceForType = function(type) {
|
loopback.getDefaultDataSourceForType = function(type) {
|
||||||
return this.defaultDataSources && this.defaultDataSources[type];
|
return this.defaultDataSources && this.defaultDataSources[type];
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach any model that does not have a dataSource to
|
* Attach any model that does not have a dataSource to
|
||||||
|
@ -265,7 +292,7 @@ loopback.autoAttach = function() {
|
||||||
loopback.autoAttachModel(ModelCtor);
|
loopback.autoAttachModel(ModelCtor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
loopback.autoAttachModel = function(ModelCtor) {
|
loopback.autoAttachModel = function(ModelCtor) {
|
||||||
if(ModelCtor.autoAttach) {
|
if(ModelCtor.autoAttach) {
|
||||||
|
@ -277,13 +304,14 @@ loopback.autoAttachModel = function(ModelCtor) {
|
||||||
|
|
||||||
ModelCtor.attachTo(ds);
|
ModelCtor.attachTo(ds);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/*
|
/*!
|
||||||
* Built in models / services
|
* Built in models / services
|
||||||
*/
|
*/
|
||||||
|
|
||||||
loopback.Model = require('./models/model');
|
loopback.Model = require('./models/model');
|
||||||
|
loopback.DataModel = require('./models/data-model');
|
||||||
loopback.Email = require('./models/email');
|
loopback.Email = require('./models/email');
|
||||||
loopback.User = require('./models/user');
|
loopback.User = require('./models/user');
|
||||||
loopback.Application = require('./models/application');
|
loopback.Application = require('./models/application');
|
||||||
|
@ -304,6 +332,7 @@ var dataSourceTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
||||||
|
loopback.DataModel.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.User.autoAttach = dataSourceTypes.DB;
|
loopback.User.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.Role.autoAttach = dataSourceTypes.DB;
|
loopback.Role.autoAttach = dataSourceTypes.DB;
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loopback = require('../loopback');
|
var loopback = require('../loopback');
|
||||||
var RemoteObjects = require('strong-remoting');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the middleware.
|
* Export the middleware.
|
||||||
|
@ -23,7 +22,7 @@ function rest() {
|
||||||
if(req.url === '/routes') {
|
if(req.url === '/routes') {
|
||||||
res.send(handler.adapter.allRoutes());
|
res.send(handler.adapter.allRoutes());
|
||||||
} else if(req.url === '/models') {
|
} else if(req.url === '/models') {
|
||||||
return res.send(remotes.toJSON());
|
return res.send(app.remotes().toJSON());
|
||||||
} else {
|
} else {
|
||||||
handler(req, res, next);
|
handler(req, res, next);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loopback = require('../loopback');
|
var loopback = require('../loopback');
|
||||||
var RemoteObjects = require('strong-remoting');
|
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
|
@ -194,8 +194,8 @@ Principal.SCOPE = 'SCOPE';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare if two principals are equal
|
* Compare if two principals are equal
|
||||||
* @param p The other principal
|
* Returns true if argument principal is equal to this principal.
|
||||||
* @returns {boolean}
|
* @param {Object} principal The other principal
|
||||||
*/
|
*/
|
||||||
Principal.prototype.equals = function (p) {
|
Principal.prototype.equals = function (p) {
|
||||||
if (p instanceof Principal) {
|
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} model The model name
|
||||||
* @param {String} property
|
* @param {String} property
|
||||||
* @param {String} accessType The access type
|
* @param {String} accessType The access type
|
||||||
* @param {String} permission The permission
|
* @param {String} permission The requested permission
|
||||||
* @returns {AccessRequest}
|
* @returns {AccessRequest}
|
||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
|
@ -217,10 +217,19 @@ function AccessRequest(model, property, accessType, permission) {
|
||||||
if (!(this instanceof AccessRequest)) {
|
if (!(this instanceof AccessRequest)) {
|
||||||
return new AccessRequest(model, property, accessType);
|
return new AccessRequest(model, property, accessType);
|
||||||
}
|
}
|
||||||
this.model = model || AccessContext.ALL;
|
if (arguments.length === 1 && typeof model === 'object') {
|
||||||
this.property = property || AccessContext.ALL;
|
// The argument is an object that contains all required properties
|
||||||
this.accessType = accessType || AccessContext.ALL;
|
var obj = model || {};
|
||||||
this.permission = permission || AccessContext.DEFAULT;
|
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) {
|
if(debug.enabled) {
|
||||||
debug('---AccessRequest---');
|
debug('---AccessRequest---');
|
||||||
|
|
|
@ -17,7 +17,7 @@ var Model = require('../loopback').Model
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var properties = {
|
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
|
ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds
|
||||||
created: {type: Date, default: function() {
|
created: {type: Date, default: function() {
|
||||||
return new Date();
|
return new Date();
|
||||||
|
@ -53,7 +53,14 @@ var AccessToken = module.exports = Model.extend('AccessToken', properties, {
|
||||||
property: 'create',
|
property: 'create',
|
||||||
permission: 'ALLOW'
|
permission: 'ALLOW'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User',
|
||||||
|
foreignKey: 'userId'
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -137,6 +137,50 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
return -1;
|
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 = score * 4;
|
||||||
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
||||||
return score;
|
return score;
|
||||||
|
@ -149,10 +193,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
* @returns {AccessRequest} result The effective ACL
|
* @returns {AccessRequest} result The effective ACL
|
||||||
*/
|
*/
|
||||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
|
if(!(req instanceof AccessRequest)) {
|
||||||
|
req = new AccessRequest(req);
|
||||||
|
}
|
||||||
// Sort by the matching score in descending order
|
// Sort by the matching score in descending order
|
||||||
acls = acls.sort(function (rule1, rule2) {
|
acls = acls.sort(function (rule1, rule2) {
|
||||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||||
});
|
});
|
||||||
|
if(debug.enabled) {
|
||||||
|
debug('ACLs by order: %j', acls);
|
||||||
|
}
|
||||||
var permission = ACL.DEFAULT;
|
var permission = ACL.DEFAULT;
|
||||||
var score = 0;
|
var score = 0;
|
||||||
for (var i = 0; i < acls.length; i++) {
|
for (var i = 0; i < acls.length; i++) {
|
||||||
|
@ -351,6 +401,9 @@ ACL.checkAccess = function (context, callback) {
|
||||||
inRoleTasks.push(function (done) {
|
inRoleTasks.push(function (done) {
|
||||||
roleModel.isInRole(acl.principalId, context,
|
roleModel.isInRole(acl.principalId, context,
|
||||||
function (err, inRole) {
|
function (err, inRole) {
|
||||||
|
if(debug.enabled) {
|
||||||
|
debug('In role %j: %j', acl.principalId, inRole);
|
||||||
|
}
|
||||||
if (!err && inRole) {
|
if (!err && inRole) {
|
||||||
effectiveACLs.push(acl);
|
effectiveACLs.push(acl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ var PushNotificationSettingSchema = {
|
||||||
* Data model for Application
|
* Data model for Application
|
||||||
*/
|
*/
|
||||||
var ApplicationSchema = {
|
var ApplicationSchema = {
|
||||||
id: {type: String, id: true, generated: true},
|
id: {type: String, id: true},
|
||||||
// Basic information
|
// Basic information
|
||||||
name: {type: String, required: true}, // The name
|
name: {type: String, required: true}, // The name
|
||||||
description: String, // The description
|
description: String, // The description
|
||||||
|
@ -90,7 +90,7 @@ var ApplicationSchema = {
|
||||||
modified: {type: Date, default: Date}
|
modified: {type: Date, default: Date}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
* Application management functions
|
* Application management functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -98,12 +98,13 @@ var crypto = require('crypto');
|
||||||
|
|
||||||
function generateKey(hmacKey, algorithm, encoding) {
|
function generateKey(hmacKey, algorithm, encoding) {
|
||||||
hmacKey = hmacKey || 'loopback';
|
hmacKey = hmacKey || 'loopback';
|
||||||
algorithm = algorithm || 'sha256';
|
algorithm = algorithm || 'sha1';
|
||||||
encoding = encoding || 'base64';
|
encoding = encoding || 'hex';
|
||||||
var hmac = crypto.createHmac(algorithm, hmacKey);
|
var hmac = crypto.createHmac(algorithm, hmacKey);
|
||||||
var buf = crypto.randomBytes(64);
|
var buf = crypto.randomBytes(32);
|
||||||
hmac.update(buf);
|
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) {
|
Application.beforeCreate = function (next) {
|
||||||
var app = this;
|
var app = this;
|
||||||
app.created = app.modified = new Date();
|
app.created = app.modified = new Date();
|
||||||
app.id = generateKey('id', 'sha1');
|
app.id = generateKey('id', 'md5');
|
||||||
app.clientKey = generateKey('client');
|
app.clientKey = generateKey('client');
|
||||||
app.javaScriptKey = generateKey('javaScript');
|
app.javaScriptKey = generateKey('javaScript');
|
||||||
app.restApiKey = generateKey('restApi');
|
app.restApiKey = generateKey('restApi');
|
||||||
|
@ -188,8 +189,7 @@ Application.resetKeys = function (appId, cb) {
|
||||||
/**
|
/**
|
||||||
* Authenticate the application id and key.
|
* Authenticate the application id and key.
|
||||||
*
|
*
|
||||||
* `matched` will be one of
|
* `matched` parameter is one of:
|
||||||
*
|
|
||||||
* - clientKey
|
* - clientKey
|
||||||
* - javaScriptKey
|
* - javaScriptKey
|
||||||
* - restApiKey
|
* - restApiKey
|
||||||
|
@ -200,7 +200,7 @@ Application.resetKeys = function (appId, cb) {
|
||||||
* @param {String} key
|
* @param {String} key
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
* @param {String} matched - The matching key
|
* @param {String} matched The matching key
|
||||||
*/
|
*/
|
||||||
Application.authenticate = function (appId, key, cb) {
|
Application.authenticate = function (appId, key, cb) {
|
||||||
this.findById(appId, function (err, app) {
|
this.findById(appId, function (err, app) {
|
||||||
|
@ -208,13 +208,18 @@ Application.authenticate = function (appId, key, cb) {
|
||||||
cb && cb(err, null);
|
cb && cb(err, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var matched = null;
|
var result = null;
|
||||||
['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function (k) {
|
var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
|
||||||
if (app[k] === key) {
|
for (var i = 0; i < keyNames.length; i++) {
|
||||||
matched = k;
|
if (app[keyNames[i]] === key) {
|
||||||
|
result = {
|
||||||
|
application: app,
|
||||||
|
keyType: keyNames[i]
|
||||||
|
};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
cb && cb(null, matched);
|
cb && cb(null, result);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,11 +18,11 @@ var properties = {
|
||||||
*
|
*
|
||||||
* **Properties**
|
* **Properties**
|
||||||
*
|
*
|
||||||
* - `to` - **{ String }** **required**
|
* - `to` - String (required)
|
||||||
* - `from` - **{ String }** **required**
|
* - `from` - String (required)
|
||||||
* - `subject` - **{ String }** **required**
|
* - `subject` - String (required)
|
||||||
* - `text` - **{ String }**
|
* - `text` - String
|
||||||
* - `html` - **{ String }**
|
* - `html` - String
|
||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
* @inherits {Model}
|
* @inherits {Model}
|
||||||
|
@ -35,19 +35,24 @@ var Email = module.exports = Model.extend('Email', properties);
|
||||||
*
|
*
|
||||||
* Example Options:
|
* Example Options:
|
||||||
*
|
*
|
||||||
* ```json
|
* ```js
|
||||||
* {
|
* {
|
||||||
* from: "Fred Foo ✔ <foo@blurdybloop.com>", // sender address
|
* from: "Fred Foo <foo@blurdybloop.com>", // sender address
|
||||||
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
|
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
|
||||||
* subject: "Hello ✔", // Subject line
|
* subject: "Hello", // Subject line
|
||||||
* text: "Hello world ✔", // plaintext body
|
* text: "Hello world", // plaintext body
|
||||||
* html: "<b>Hello world ✔</b>" // html body
|
* html: "<b>Hello world</b>" // html body
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* See https://github.com/andris9/Nodemailer for other supported options.
|
* 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
|
* @param {Function} callback Called after the e-mail is sent or the sending failed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
var loopback = require('../loopback');
|
var loopback = require('../loopback');
|
||||||
|
var compat = require('../compat');
|
||||||
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
|
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
|
||||||
var modeler = new ModelBuilder();
|
var modeler = new ModelBuilder();
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
@ -133,7 +134,8 @@ Model.setup = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
if(this.app) {
|
if(this.app) {
|
||||||
var remotes = this.app.remotes();
|
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);
|
fn(ctx, ctx.result, next);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -149,7 +151,8 @@ Model.setup = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
if(this.app) {
|
if(this.app) {
|
||||||
var remotes = this.app.remotes();
|
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);
|
fn(ctx, ctx.result, next);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -161,8 +164,9 @@ Model.setup = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map the prototype method to /:id with data in the body
|
// Map the prototype method to /:id with data in the body
|
||||||
|
var idDesc = ModelCtor.modelName + ' id';
|
||||||
ModelCtor.sharedCtor.accepts = [
|
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'}}
|
// {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
|
* Get the reference to ACL in a lazy fashion to avoid race condition in require
|
||||||
*/
|
*/
|
||||||
var ACL = null;
|
var _aclModel = null;
|
||||||
function getACL() {
|
Model._ACL = function getACL(ACL) {
|
||||||
return ACL || (ACL = require('./acl').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
|
* Check if the given access token can invoke the method
|
||||||
|
@ -206,9 +220,9 @@ function getACL() {
|
||||||
Model.checkAccess = function(token, modelId, method, callback) {
|
Model.checkAccess = function(token, modelId, method, callback) {
|
||||||
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||||
token = token || ANONYMOUS;
|
token = token || ANONYMOUS;
|
||||||
var ACL = getACL();
|
var aclModel = Model._ACL();
|
||||||
var methodName = 'string' === typeof method? method: method && method.name;
|
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'
|
'method is a required argument and must be a RemoteMethod object'
|
||||||
);
|
);
|
||||||
|
|
||||||
var ACL = getACL();
|
var ACL = Model._ACL();
|
||||||
|
|
||||||
switch(method.name) {
|
switch(method.name) {
|
||||||
case'create':
|
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
|
// setup the initial model
|
||||||
Model.setup();
|
Model.setup();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -16,7 +16,7 @@ var RoleSchema = {
|
||||||
modified: {type: Date, default: Date}
|
modified: {type: Date, default: Date}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
* Map principals to roles
|
* Map principals to roles
|
||||||
*/
|
*/
|
||||||
var RoleMappingSchema = {
|
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, {
|
var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, {
|
||||||
|
@ -196,6 +199,21 @@ function isUserClass(modelClass) {
|
||||||
modelClass.prototype instanceof loopback.User;
|
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
|
* Check if a given userId is the owner the model instance
|
||||||
* @param {Function} modelClass The model class
|
* @param {Function} modelClass The model class
|
||||||
|
@ -205,7 +223,7 @@ function isUserClass(modelClass) {
|
||||||
*/
|
*/
|
||||||
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
assert(modelClass, 'Model class is required');
|
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
|
// No userId is present
|
||||||
if(!userId) {
|
if(!userId) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
|
@ -217,19 +235,21 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
// Is the modelClass User or a subclass of User?
|
// Is the modelClass User or a subclass of User?
|
||||||
if(isUserClass(modelClass)) {
|
if(isUserClass(modelClass)) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback(null, modelId == userId);
|
callback(null, matches(modelId, userId));
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modelClass.findById(modelId, function(err, inst) {
|
modelClass.findById(modelId, function(err, inst) {
|
||||||
if(err || !inst) {
|
if(err || !inst) {
|
||||||
|
debug('Model not found for id %j', modelId);
|
||||||
callback && callback(err, false);
|
callback && callback(err, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug('Model found: %j', inst);
|
debug('Model found: %j', inst);
|
||||||
if(inst.userId || inst.owner) {
|
var ownerId = inst.userId || inst.owner;
|
||||||
callback && callback(null, (inst.userId || inst.owner) === userId);
|
if(ownerId) {
|
||||||
|
callback && callback(null, matches(ownerId, userId));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Try to follow belongsTo
|
// Try to follow belongsTo
|
||||||
|
@ -240,7 +260,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
inst[r](function(err, user) {
|
inst[r](function(err, user) {
|
||||||
if(!err && user) {
|
if(!err && user) {
|
||||||
debug('User found: %j', user.id);
|
debug('User found: %j', user.id);
|
||||||
callback && callback(null, user.id === userId);
|
callback && callback(null, matches(user.id, userId));
|
||||||
} else {
|
} else {
|
||||||
callback && callback(err, false);
|
callback && callback(err, false);
|
||||||
}
|
}
|
||||||
|
@ -248,6 +268,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId);
|
||||||
callback && callback(null, false);
|
callback && callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -401,6 +422,9 @@ Role.getRoles = function (context, callback) {
|
||||||
Object.keys(Role.resolvers).forEach(function (role) {
|
Object.keys(Role.resolvers).forEach(function (role) {
|
||||||
inRoleTasks.push(function (done) {
|
inRoleTasks.push(function (done) {
|
||||||
self.isInRole(role, context, function (err, inRole) {
|
self.isInRole(role, context, function (err, inRole) {
|
||||||
|
if(debug.enabled) {
|
||||||
|
debug('In role %j: %j', role, inRole);
|
||||||
|
}
|
||||||
if (!err && inRole) {
|
if (!err && inRole) {
|
||||||
addRole(role);
|
addRole(role);
|
||||||
done();
|
done();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/**
|
/*!
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -18,7 +18,9 @@ var Model = require('../loopback').Model
|
||||||
, ACL = require('./acl').ACL
|
, ACL = require('./acl').ACL
|
||||||
, assert = require('assert');
|
, assert = require('assert');
|
||||||
|
|
||||||
/**
|
var debug = require('debug')('loopback:user');
|
||||||
|
|
||||||
|
/*!
|
||||||
* Default User properties.
|
* Default User properties.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -48,11 +50,12 @@ var properties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
|
hidden: ['password'],
|
||||||
acls: [
|
acls: [
|
||||||
{
|
{
|
||||||
principalType: ACL.ROLE,
|
principalType: ACL.ROLE,
|
||||||
principalId: Role.EVERYONE,
|
principalId: Role.EVERYONE,
|
||||||
permission: ACL.DENY,
|
permission: ACL.DENY
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
principalType: ACL.ROLE,
|
principalType: ACL.ROLE,
|
||||||
|
@ -89,15 +92,28 @@ var options = {
|
||||||
principalId: Role.OWNER,
|
principalId: Role.OWNER,
|
||||||
permission: ACL.ALLOW,
|
permission: ACL.ALLOW,
|
||||||
property: "updateAttributes"
|
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.
|
* Extends from the built in `loopback.Model` type.
|
||||||
*
|
*
|
||||||
* Default `User` ACLs.
|
* Default `User` ACLs.
|
||||||
*
|
*
|
||||||
* - DENY EVERYONE `*`
|
* - DENY EVERYONE `*`
|
||||||
* - ALLOW EVERYONE `create`
|
* - ALLOW EVERYONE `create`
|
||||||
* - ALLOW OWNER `removeById`
|
* - ALLOW OWNER `removeById`
|
||||||
|
@ -127,40 +143,64 @@ var User = module.exports = Model.extend('User', properties, options);
|
||||||
* @param {AccessToken} token
|
* @param {AccessToken} token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.login = function (credentials, fn) {
|
User.login = function (credentials, include, fn) {
|
||||||
var UserCtor = this;
|
if (typeof include === 'function') {
|
||||||
|
fn = include;
|
||||||
|
include = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
include = (include || '').toLowerCase();
|
||||||
|
|
||||||
var query = {};
|
var query = {};
|
||||||
|
|
||||||
if(credentials.email) {
|
if(credentials.email) {
|
||||||
query.email = credentials.email;
|
query.email = credentials.email;
|
||||||
} else if(credentials.username) {
|
} else if(credentials.username) {
|
||||||
query.username = credentials.username;
|
query.username = credentials.username;
|
||||||
} else {
|
} 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) {
|
this.findOne({where: query}, function(err, user) {
|
||||||
var defaultError = new Error('login failed');
|
var defaultError = new Error('login failed');
|
||||||
|
defaultError.statusCode = 401;
|
||||||
|
|
||||||
if(err) {
|
if(err) {
|
||||||
|
debug('An error is reported from User.findOne: %j', err);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
} else if(user) {
|
} else if(user) {
|
||||||
user.hasPassword(credentials.password, function(err, isMatch) {
|
user.hasPassword(credentials.password, function(err, isMatch) {
|
||||||
if(err) {
|
if(err) {
|
||||||
|
debug('An error is reported from User.hasPassword: %j', err);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
} else if(isMatch) {
|
} else if(isMatch) {
|
||||||
user.accessTokens.create({
|
user.accessTokens.create({
|
||||||
ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL)
|
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 {
|
} else {
|
||||||
|
debug('The password is invalid for user %s', query.email || query.username);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
debug('No matching record is found for user %s', query.email || query.username);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout a user with the given accessToken id.
|
* 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.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.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');
|
assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property');
|
||||||
|
|
||||||
options.redirect = options.redirect || '/';
|
options.redirect = options.redirect || '/';
|
||||||
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
||||||
options.user = this;
|
options.user = this;
|
||||||
|
@ -240,40 +280,44 @@ User.prototype.verify = function (options, fn) {
|
||||||
options.protocol
|
options.protocol
|
||||||
+ '://'
|
+ '://'
|
||||||
+ options.host
|
+ options.host
|
||||||
+ (User.sharedCtor.http.path || '/' + User.pluralModelName)
|
+ User.http.path
|
||||||
+ User.confirm.http.path;
|
+ User.confirm.http.path
|
||||||
|
+ '?uid='
|
||||||
|
+ options.user.id
|
||||||
|
+ '&redirect='
|
||||||
|
+ options.redirect;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Email model
|
// Email model
|
||||||
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
||||||
|
|
||||||
crypto.randomBytes(64, function(err, buf) {
|
crypto.randomBytes(64, function(err, buf) {
|
||||||
if(err) {
|
if(err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
user.verificationToken = buf.toString('base64');
|
user.verificationToken = buf.toString('hex');
|
||||||
user.save(function (err) {
|
user.save(function (err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
sendEmail(user);
|
sendEmail(user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO - support more verification types
|
// TODO - support more verification types
|
||||||
function sendEmail(user) {
|
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 || 'Please verify your email by opening this link in a web browser:\n\t{href}';
|
||||||
|
|
||||||
options.text = options.text.replace('{href}', options.verifyHref);
|
options.text = options.text.replace('{href}', options.verifyHref);
|
||||||
|
|
||||||
var template = loopback.template(options.template);
|
var template = loopback.template(options.template);
|
||||||
Email.send({
|
Email.send({
|
||||||
to: options.to || user.email,
|
to: options.to || user.email,
|
||||||
|
from: options.from,
|
||||||
subject: options.subject || 'Thanks for Registering',
|
subject: options.subject || 'Thanks for Registering',
|
||||||
text: options.text,
|
text: options.text,
|
||||||
html: template(options)
|
html: template(options)
|
||||||
|
@ -372,7 +416,7 @@ User.setup = function () {
|
||||||
// We need to call the base class's setup method
|
// We need to call the base class's setup method
|
||||||
Model.setup.call(this);
|
Model.setup.call(this);
|
||||||
var UserModel = this;
|
var UserModel = this;
|
||||||
|
|
||||||
// max ttl
|
// max ttl
|
||||||
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
||||||
this.settings.ttl = DEFAULT_TTL;
|
this.settings.ttl = DEFAULT_TTL;
|
||||||
|
@ -381,18 +425,27 @@ User.setup = function () {
|
||||||
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
||||||
this.$password = bcrypt.hashSync(plain, salt);
|
this.$password = bcrypt.hashSync(plain, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.login,
|
UserModel.login,
|
||||||
{
|
{
|
||||||
accepts: [
|
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'}
|
http: {verb: 'post'}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.logout,
|
UserModel.logout,
|
||||||
{
|
{
|
||||||
|
@ -403,12 +456,15 @@ User.setup = function () {
|
||||||
var tokenID = accessToken && accessToken.id;
|
var tokenID = accessToken && accessToken.id;
|
||||||
|
|
||||||
return tokenID;
|
return tokenID;
|
||||||
}}
|
}, description:
|
||||||
|
'Do not supply this argument, it is automatically extracted ' +
|
||||||
|
'from request headers.'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
http: {verb: 'all'}
|
http: {verb: 'all'}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.confirm,
|
UserModel.confirm,
|
||||||
{
|
{
|
||||||
|
@ -420,7 +476,7 @@ User.setup = function () {
|
||||||
http: {verb: 'get', path: '/confirm'}
|
http: {verb: 'get', path: '/confirm'}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.resetPassword,
|
UserModel.resetPassword,
|
||||||
{
|
{
|
||||||
|
@ -440,16 +496,16 @@ User.setup = function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// default models
|
// default models
|
||||||
UserModel.email = require('./email');
|
UserModel.email = require('./email');
|
||||||
UserModel.accessToken = require('./access-token');
|
UserModel.accessToken = require('./access-token');
|
||||||
|
|
||||||
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
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,}))$/;
|
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'});
|
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||||
|
|
||||||
return UserModel;
|
return UserModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
61
package.json
61
package.json
|
@ -9,40 +9,65 @@
|
||||||
"Platform",
|
"Platform",
|
||||||
"mBaaS"
|
"mBaaS"
|
||||||
],
|
],
|
||||||
"version": "1.5.3",
|
"version": "1.7.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha -R spec"
|
"test": "mocha -R spec"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "~0.7.2",
|
"debug": "~0.7.4",
|
||||||
"express": "~3.4.0",
|
"express": "~3.4.8",
|
||||||
"strong-remoting": "~1.1.0",
|
"strong-remoting": "~1.3.1",
|
||||||
"inflection": "~1.2.5",
|
"inflection": "~1.3.5",
|
||||||
"passport": "~0.1.17",
|
"passport": "~0.2.0",
|
||||||
"passport-local": "~0.1.6",
|
"passport-local": "~0.1.6",
|
||||||
"nodemailer": "~0.5.7",
|
"nodemailer": "~0.6.0",
|
||||||
"ejs": "~0.8.4",
|
"ejs": "~0.8.5",
|
||||||
"bcryptjs": "~0.7.10",
|
"bcryptjs": "~0.7.12",
|
||||||
"underscore.string": "~2.3.3",
|
"underscore.string": "~2.3.3",
|
||||||
"underscore": "~1.5.2",
|
"underscore": "~1.6.0",
|
||||||
"uid2": "0.0.3",
|
"uid2": "0.0.3",
|
||||||
"async": "~0.2.9",
|
"async": "~0.2.10",
|
||||||
"canonical-json": "0.0.3"
|
"canonical-json": "0.0.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"loopback-datasource-juggler": "~1.2.11"
|
"loopback-datasource-juggler": "~1.3.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"loopback-datasource-juggler": "~1.2.11",
|
"loopback-datasource-juggler": "~1.3.11",
|
||||||
"mocha": "~1.14.0",
|
"mocha": "~1.17.1",
|
||||||
"strong-task-emitter": "0.0.x",
|
"strong-task-emitter": "0.0.x",
|
||||||
"supertest": "~0.8.1",
|
"supertest": "~0.9.0",
|
||||||
"chai": "~1.8.1",
|
"chai": "~1.9.0",
|
||||||
"loopback-testing": "~0.1.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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/strongloop/loopback"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
1139
test/README.md
1139
test/README.md
File diff suppressed because it is too large
Load Diff
|
@ -11,6 +11,7 @@ describe('access control - integration', function () {
|
||||||
|
|
||||||
lt.beforeEach.withApp(app);
|
lt.beforeEach.withApp(app);
|
||||||
|
|
||||||
|
/*
|
||||||
describe('accessToken', function() {
|
describe('accessToken', function() {
|
||||||
// it('should be a sublcass of AccessToken', function () {
|
// it('should be a sublcass of AccessToken', function () {
|
||||||
// assert(app.models.accessToken.prototype instanceof loopback.AccessToken);
|
// assert(app.models.accessToken.prototype instanceof loopback.AccessToken);
|
||||||
|
@ -54,6 +55,7 @@ describe('access control - integration', function () {
|
||||||
return '/api/accessTokens/' + this.randomToken.id;
|
return '/api/accessTokens/' + this.randomToken.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
describe('/users', function () {
|
describe('/users', function () {
|
||||||
|
|
||||||
|
@ -93,6 +95,10 @@ describe('access control - integration', function () {
|
||||||
});
|
});
|
||||||
lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() {
|
lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() {
|
||||||
lt.it.shouldBeAllowed();
|
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.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
|
||||||
lt.it.shouldBeAllowed();
|
lt.it.shouldBeAllowed();
|
||||||
|
@ -147,6 +153,7 @@ describe('access control - integration', function () {
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
|
||||||
|
|
||||||
|
|
||||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, '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.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
|
||||||
|
|
||||||
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
||||||
beforeEach(function() {
|
beforeEach(function(done) {
|
||||||
this.url = '/api/accounts/' + this.user.accountId;
|
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.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
|
||||||
lt.it.shouldBeAllowed();
|
lt.it.shouldBeAllowed();
|
||||||
});
|
});
|
||||||
|
lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() {
|
||||||
|
lt.it.shouldBeAllowed();
|
||||||
|
});
|
||||||
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
|
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
|
||||||
lt.it.shouldBeDenied();
|
lt.it.shouldBeDenied();
|
||||||
});
|
});
|
||||||
|
|
|
@ -70,6 +70,40 @@ describe('security scopes', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('security ACLs', 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 () {
|
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,
|
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
|
||||||
|
|
110
test/app.test.js
110
test/app.test.js
|
@ -4,12 +4,60 @@ var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||||
describe('app', function() {
|
describe('app', function() {
|
||||||
|
|
||||||
describe('app.model(Model)', 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() {
|
it("Expose a `Model` to remote clients", function() {
|
||||||
var app = loopback();
|
var Color = db.createModel('color', {name: String});
|
||||||
var memory = loopback.createDataSource({connector: loopback.Memory});
|
|
||||||
var Color = memory.createModel('color', {name: String});
|
|
||||||
app.model(Color);
|
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 () {
|
describe('app.boot([options])', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
var app = this.app = loopback();
|
|
||||||
|
|
||||||
app.boot({
|
app.boot({
|
||||||
app: {
|
app: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
restApiRoot: '/rest-api',
|
restApiRoot: '/rest-api',
|
||||||
foo: {bar: 'bat'},
|
foo: {bar: 'bat'},
|
||||||
|
@ -59,7 +114,7 @@ describe('app', function() {
|
||||||
dataSources: {
|
dataSources: {
|
||||||
'the-db': {
|
'the-db': {
|
||||||
connector: 'memory'
|
connector: 'memory'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -106,14 +161,14 @@ describe('app', function() {
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
app.boot({
|
app.boot({
|
||||||
app: {
|
app: {
|
||||||
port: undefined,
|
port: undefined,
|
||||||
host: undefined
|
host: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be honored', function() {
|
it('should be honored', function() {
|
||||||
var assertHonored = function (portKey, hostKey) {
|
var assertHonored = function (portKey, hostKey) {
|
||||||
process.env[hostKey] = randomPort();
|
process.env[hostKey] = randomPort();
|
||||||
|
@ -142,6 +197,12 @@ describe('app', function() {
|
||||||
var app = this.boot();
|
var app = this.boot();
|
||||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
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.npm_config_port = randomPort();
|
||||||
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
||||||
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
||||||
|
@ -151,6 +212,12 @@ describe('app', function() {
|
||||||
var app = this.boot();
|
var app = this.boot();
|
||||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||||
assert.equal(app.get('port'), process.env.npm_config_port);
|
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() {
|
function randomHost() {
|
||||||
|
@ -160,6 +227,18 @@ describe('app', function() {
|
||||||
function randomPort() {
|
function randomPort() {
|
||||||
return Math.floor(Math.random() * 10000);
|
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 () {
|
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 () {
|
describe('app.get("/", loopback.status())', function () {
|
||||||
it('should return the status of the application', function (done) {
|
it('should return the status of the application', function (done) {
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
|
@ -365,7 +452,8 @@ describe('app', function() {
|
||||||
|
|
||||||
assert.equal(typeof res.body, 'object');
|
assert.equal(typeof res.body, 'object');
|
||||||
assert(res.body.started);
|
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));
|
var elapsed = Date.now() - Number(new Date(res.body.started));
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -23,10 +23,6 @@
|
||||||
"type": "hasMany",
|
"type": "hasMany",
|
||||||
"foreignKey": "userId"
|
"foreignKey": "userId"
|
||||||
},
|
},
|
||||||
"account": {
|
|
||||||
"model": "account",
|
|
||||||
"type": "belongsTo"
|
|
||||||
},
|
|
||||||
"transactions": {
|
"transactions": {
|
||||||
"model": "transaction",
|
"model": "transaction",
|
||||||
"type": "hasMany"
|
"type": "hasMany"
|
||||||
|
@ -103,6 +99,11 @@
|
||||||
"transactions": {
|
"transactions": {
|
||||||
"model": "transaction",
|
"model": "transaction",
|
||||||
"type": "hasMany"
|
"type": "hasMany"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"model": "user",
|
||||||
|
"type": "belongsTo",
|
||||||
|
"foreignKey": "userId"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"acls": [
|
"acls": [
|
||||||
|
|
|
@ -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());
|
|
@ -0,0 +1,4 @@
|
||||||
|
var loopback = require('../../../');
|
||||||
|
var DataModel = loopback.DataModel;
|
||||||
|
|
||||||
|
exports.TestModel = DataModel.extend('TestModel');
|
|
@ -7,7 +7,6 @@ app.use(loopback.favicon());
|
||||||
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
||||||
var apiPath = '/api';
|
var apiPath = '/api';
|
||||||
app.use(apiPath, loopback.rest());
|
app.use(apiPath, loopback.rest());
|
||||||
app.use(app.router);
|
|
||||||
app.use(loopback.static(path.join(__dirname, 'public')));
|
app.use(loopback.static(path.join(__dirname, 'public')));
|
||||||
app.use(loopback.urlNotFound());
|
app.use(loopback.urlNotFound());
|
||||||
app.use(loopback.errorHandler());
|
app.use(loopback.errorHandler());
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
{
|
{
|
||||||
"port": 3000,
|
"port": 3000,
|
||||||
"host": "0.0.0.0",
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -30,7 +30,15 @@
|
||||||
"widget": {
|
"widget": {
|
||||||
"properties": {},
|
"properties": {},
|
||||||
"public": true,
|
"public": true,
|
||||||
"dataSource": "db"
|
"dataSource": "db",
|
||||||
|
"options": {
|
||||||
|
"relations": {
|
||||||
|
"store": {
|
||||||
|
"model": "store",
|
||||||
|
"type": "belongsTo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"store": {
|
"store": {
|
||||||
"properties": {},
|
"properties": {},
|
||||||
|
|
|
@ -53,4 +53,4 @@ describe('GeoPoint', function() {
|
||||||
assert.equal(m.geo.lat, 3.444);
|
assert.equal(m.geo.lat, 3.444);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -20,6 +20,7 @@ describe('Application', function () {
|
||||||
assert(app.masterKey);
|
assert(app.masterKey);
|
||||||
assert(app.created);
|
assert(app.created);
|
||||||
assert(app.modified);
|
assert(app.modified);
|
||||||
|
assert.equal(typeof app.id, 'string');
|
||||||
done(err, result);
|
done(err, result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -121,7 +122,8 @@ describe('Application', function () {
|
||||||
it('Authenticate with application id & clientKey', function (done) {
|
it('Authenticate with application id & clientKey', function (done) {
|
||||||
Application.authenticate(registeredApp.id, registeredApp.clientKey,
|
Application.authenticate(registeredApp.id, registeredApp.clientKey,
|
||||||
function (err, result) {
|
function (err, result) {
|
||||||
assert.equal(result, 'clientKey');
|
assert.equal(result.application.id, registeredApp.id);
|
||||||
|
assert.equal(result.keyType, 'clientKey');
|
||||||
done(err, result);
|
done(err, result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -129,7 +131,8 @@ describe('Application', function () {
|
||||||
it('Authenticate with application id & javaScriptKey', function (done) {
|
it('Authenticate with application id & javaScriptKey', function (done) {
|
||||||
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey,
|
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey,
|
||||||
function (err, result) {
|
function (err, result) {
|
||||||
assert.equal(result, 'javaScriptKey');
|
assert.equal(result.application.id, registeredApp.id);
|
||||||
|
assert.equal(result.keyType, 'javaScriptKey');
|
||||||
done(err, result);
|
done(err, result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -137,7 +140,8 @@ describe('Application', function () {
|
||||||
it('Authenticate with application id & restApiKey', function (done) {
|
it('Authenticate with application id & restApiKey', function (done) {
|
||||||
Application.authenticate(registeredApp.id, registeredApp.restApiKey,
|
Application.authenticate(registeredApp.id, registeredApp.restApiKey,
|
||||||
function (err, result) {
|
function (err, result) {
|
||||||
assert.equal(result, 'restApiKey');
|
assert.equal(result.application.id, registeredApp.id);
|
||||||
|
assert.equal(result.keyType, 'restApiKey');
|
||||||
done(err, result);
|
done(err, result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -145,7 +149,8 @@ describe('Application', function () {
|
||||||
it('Authenticate with application id & masterKey', function (done) {
|
it('Authenticate with application id & masterKey', function (done) {
|
||||||
Application.authenticate(registeredApp.id, registeredApp.masterKey,
|
Application.authenticate(registeredApp.id, registeredApp.masterKey,
|
||||||
function (err, result) {
|
function (err, result) {
|
||||||
assert.equal(result, 'masterKey');
|
assert.equal(result.application.id, registeredApp.id);
|
||||||
|
assert.equal(result.keyType, 'masterKey');
|
||||||
done(err, result);
|
done(err, result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -153,7 +158,8 @@ describe('Application', function () {
|
||||||
it('Authenticate with application id & windowsKey', function (done) {
|
it('Authenticate with application id & windowsKey', function (done) {
|
||||||
Application.authenticate(registeredApp.id, registeredApp.windowsKey,
|
Application.authenticate(registeredApp.id, registeredApp.windowsKey,
|
||||||
function (err, result) {
|
function (err, result) {
|
||||||
assert.equal(result, 'windowsKey');
|
assert.equal(result.application.id, registeredApp.id);
|
||||||
|
assert.equal(result.keyType, 'windowsKey');
|
||||||
done(err, result);
|
done(err, result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -170,13 +176,14 @@ describe('Application', function () {
|
||||||
describe('Application subclass', function () {
|
describe('Application subclass', function () {
|
||||||
it('should use subclass model name', function (done) {
|
it('should use subclass model name', function (done) {
|
||||||
var MyApp = Application.extend('MyApp');
|
var MyApp = Application.extend('MyApp');
|
||||||
MyApp.attachTo(loopback.createDataSource({connector: loopback.Memory}));
|
var ds = loopback.createDataSource({connector: loopback.Memory});
|
||||||
MyApp.register('rfeng', 'MyApp2',
|
MyApp.attachTo(ds);
|
||||||
{description: 'My second mobile application'}, function (err, result) {
|
MyApp.register('rfeng', 'MyApp123',
|
||||||
|
{description: 'My 123 mobile application'}, function (err, result) {
|
||||||
var app = result;
|
var app = result;
|
||||||
assert.equal(app.owner, 'rfeng');
|
assert.equal(app.owner, 'rfeng');
|
||||||
assert.equal(app.name, 'MyApp2');
|
assert.equal(app.name, 'MyApp123');
|
||||||
assert.equal(app.description, 'My second mobile application');
|
assert.equal(app.description, 'My 123 mobile application');
|
||||||
assert(app.clientKey);
|
assert(app.clientKey);
|
||||||
assert(app.javaScriptKey);
|
assert(app.javaScriptKey);
|
||||||
assert(app.restApiKey);
|
assert(app.restApiKey);
|
||||||
|
@ -184,14 +191,17 @@ describe('Application subclass', function () {
|
||||||
assert(app.masterKey);
|
assert(app.masterKey);
|
||||||
assert(app.created);
|
assert(app.created);
|
||||||
assert(app.modified);
|
assert(app.modified);
|
||||||
MyApp.findById(app.id, function (err, myApp) {
|
// Remove all instances from Application model to avoid left-over data
|
||||||
assert(!err);
|
Application.destroyAll(function () {
|
||||||
assert(myApp);
|
MyApp.findById(app.id, function (err, myApp) {
|
||||||
|
|
||||||
Application.findById(app.id, function (err, myApp) {
|
|
||||||
assert(!err);
|
assert(!err);
|
||||||
assert(myApp === null);
|
assert(myApp);
|
||||||
done(err, myApp);
|
|
||||||
|
Application.findById(app.id, function (err, myApp) {
|
||||||
|
assert(!err);
|
||||||
|
assert(myApp === null);
|
||||||
|
done(err, myApp);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
require('./support');
|
||||||
var loopback = require('../');
|
var loopback = require('../');
|
||||||
var ACL = loopback.ACL;
|
var ACL = loopback.ACL;
|
||||||
var Change = loopback.Change;
|
var Change = loopback.Change;
|
||||||
|
@ -197,21 +198,21 @@ describe('Model', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model.deleteById([callback])', function () {
|
describe('Model.deleteById([callback])', function () {
|
||||||
it("Delete a model instance from the attached data source", function (done) {
|
it("Delete a model instance from the attached data source", function (done) {
|
||||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||||
User.deleteById(user.id, function (err) {
|
User.deleteById(user.id, function (err) {
|
||||||
User.findById(user.id, function (err, notFound) {
|
User.findById(user.id, function (err, notFound) {
|
||||||
assert(!err);
|
assert(!err);
|
||||||
assert.equal(notFound, null);
|
assert.equal(notFound, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model.destroyAll(callback)', function() {
|
describe('Model.destroyAll(callback)', function() {
|
||||||
it("Delete all Model instances from data source", function(done) {
|
it("Delete all Model instances from data source", function(done) {
|
||||||
(new TaskEmitter())
|
(new TaskEmitter())
|
||||||
.task(User, 'create', {first: 'jill'})
|
.task(User, 'create', {first: 'jill'})
|
||||||
|
@ -263,7 +264,8 @@ describe('Model', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Remote Methods', function(){
|
describe.onServer('Remote Methods', function(){
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
User.login = function (username, password, fn) {
|
User.login = function (username, password, fn) {
|
||||||
if(username === 'foo' && password === 'bar') {
|
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() {
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ var path = require('path');
|
||||||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app');
|
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app');
|
||||||
var app = require(path.join(SIMPLE_APP, 'app.js'));
|
var app = require(path.join(SIMPLE_APP, 'app.js'));
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var expect = require('chai').expect;
|
||||||
|
|
||||||
describe('relations - integration', function () {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -16,6 +16,8 @@ describe('role model', function () {
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ds = loopback.createDataSource({connector: 'memory'});
|
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);
|
User.attachTo(ds);
|
||||||
Role.attachTo(ds);
|
Role.attachTo(ds);
|
||||||
RoleMapping.attachTo(ds);
|
RoleMapping.attachTo(ds);
|
||||||
|
|
|
@ -11,13 +11,12 @@ app = null;
|
||||||
TaskEmitter = require('strong-task-emitter');
|
TaskEmitter = require('strong-task-emitter');
|
||||||
request = require('supertest');
|
request = require('supertest');
|
||||||
|
|
||||||
|
|
||||||
// Speed up the password hashing algorithm
|
// Speed up the password hashing algorithm
|
||||||
// for tests using the built-in User model
|
// for tests using the built-in User model
|
||||||
loopback.User.settings.saltWorkFactor = 4;
|
loopback.User.settings.saltWorkFactor = 4;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
app = loopback();
|
this.app = app = loopback();
|
||||||
|
|
||||||
// setup default data sources
|
// setup default data sources
|
||||||
loopback.setDefaultDataSourceForType('db', {
|
loopback.setDefaultDataSourceForType('db', {
|
||||||
|
@ -50,3 +49,19 @@ assert.isFunc = function (obj, name) {
|
||||||
assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist');
|
assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist');
|
||||||
assert(typeof obj[name] === 'function', name + ' is not a function');
|
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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -4,20 +4,25 @@ var passport = require('passport');
|
||||||
var MailConnector = require('../lib/connectors/mail');
|
var MailConnector = require('../lib/connectors/mail');
|
||||||
|
|
||||||
var userMemory = loopback.createDataSource({
|
var userMemory = loopback.createDataSource({
|
||||||
connector: loopback.Memory
|
connector: 'memory'
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('User', function(){
|
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() {
|
beforeEach(function() {
|
||||||
User = loopback.User.extend('user');
|
User = loopback.User.extend('user');
|
||||||
User.email = loopback.Email.extend('email');
|
User.email = loopback.Email.extend('email');
|
||||||
loopback.autoAttach();
|
loopback.autoAttach();
|
||||||
|
|
||||||
|
// Update the AccessToken relation to use the subclass of User
|
||||||
|
AccessToken.belongsTo(User);
|
||||||
|
|
||||||
// allow many User.afterRemote's to be called
|
// allow many User.afterRemote's to be called
|
||||||
User.setMaxListeners(0);
|
User.setMaxListeners(0);
|
||||||
|
|
||||||
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
|
|
||||||
AccessToken.belongsTo(User);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
|
@ -25,7 +30,7 @@ describe('User', function(){
|
||||||
app.use(loopback.rest());
|
app.use(loopback.rest());
|
||||||
app.model(User);
|
app.model(User);
|
||||||
|
|
||||||
User.create({email: 'foo@bar.com', password: 'bar'}, done);
|
User.create(validCredentials, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(function (done) {
|
||||||
|
@ -105,7 +110,7 @@ describe('User', function(){
|
||||||
|
|
||||||
describe('User.login', function() {
|
describe('User.login', function() {
|
||||||
it('Login a user by providing credentials', function(done) {
|
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.userId);
|
||||||
assert(accessToken.id);
|
assert(accessToken.id);
|
||||||
assert.equal(accessToken.id.length, 64);
|
assert.equal(accessToken.id.length, 64);
|
||||||
|
@ -119,7 +124,7 @@ describe('User', function(){
|
||||||
.post('/users/login')
|
.post('/users/login')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.send({email: 'foo@bar.com', password: 'bar'})
|
.send(validCredentials)
|
||||||
.end(function(err, res){
|
.end(function(err, res){
|
||||||
if(err) return done(err);
|
if(err) return done(err);
|
||||||
var accessToken = res.body;
|
var accessToken = res.body;
|
||||||
|
@ -127,11 +132,62 @@ describe('User', function(){
|
||||||
assert(accessToken.userId);
|
assert(accessToken.userId);
|
||||||
assert(accessToken.id);
|
assert(accessToken.id);
|
||||||
assert.equal(accessToken.id.length, 64);
|
assert.equal(accessToken.id.length, 64);
|
||||||
|
assert(accessToken.user === undefined);
|
||||||
|
|
||||||
done();
|
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) {
|
it('Login should only allow correct credentials', function(done) {
|
||||||
User.create({email: 'foo22@bar.com', password: 'bar'}, function(user, err) {
|
User.create({email: 'foo22@bar.com', password: 'bar'}, function(user, err) {
|
||||||
User.login({email: 'foo44@bar.com', password: 'bar'}, function(err, accessToken) {
|
User.login({email: 'foo44@bar.com', password: 'bar'}, function(err, accessToken) {
|
||||||
|
|
Loading…
Reference in New Issue