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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -17,3 +23,289 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
StrongLoop License
|
||||
|
||||
STRONGLOOP SUBSCRIPTION AGREEMENT
|
||||
PLEASE READ THIS AGREEMENT CAREFULLY BEFORE YOU AGREE TO THESE TERMS. IF YOU
|
||||
ARE ACTING ON BEHALF OF AN ENTITY, THEN YOU REPRESENT THAT YOU HAVE THE
|
||||
AUTHORITY TO ENTER INTO THIS AGREEMENT ON BEHALF OF THAT ENTITY. IF YOU DO NOT
|
||||
AGREE TO THESE TERMS, YOU SHOULD NOT AGREE TO THE TERMS OF THIS AGREEMENT OR
|
||||
INSTALL OR USE THE SOFTWARE.
|
||||
This StrongLoop Subscription Agreement ("Agreement") is made by and between
|
||||
StrongLoop, Inc. ("StrongLoop") with its principal place of business at 107 S.
|
||||
B St, Suite 220, San Mateo, CA 94401 and the person or entity entering into this
|
||||
Agreement ("Customer"). The effective date ("Effective Date") of this Agreement
|
||||
is the date Customer agrees to these terms or installs or uses the Software (as
|
||||
defined below). This Agreement applies to Customer's use of the Software but it
|
||||
shall be superseded by any signed agreement between you and StrongLoop
|
||||
concerning the Software.
|
||||
1. Subscriptions and Licenses.
|
||||
1.1 Subscriptions. StrongLoop offers five different subscription levels to its
|
||||
customers, each as more particularly described on StrongLoop's website located
|
||||
at www.strongloop.com (the "StrongLoop Site"): (1) Free; (2) Developer; (3)
|
||||
Professional; (4) Gold; and (5) Platinum. The actual subscription level
|
||||
applicable to Customer (the "Subscription") will be specified in the purchase
|
||||
order that Customer issues to StrongLoop. This Agreement applies to Customer
|
||||
regardless of the level of the Subscription selected by Customer and whether or
|
||||
not Customer upgrades or downgrades its Subscription. StrongLoop hereby agrees
|
||||
to provide the services as described on the StrongLoop Site for each
|
||||
Subscription level during the term for which Customer has purchased the
|
||||
applicable Subscription, subject to Customer paying the fees applicable to the
|
||||
Subscription level purchased, if any (the "Subscription Fees"). StrongLoop may
|
||||
modify the services to be provided under any Subscription upon notice to
|
||||
Customer.
|
||||
1.2 License Grant. Subject to the terms and conditions of this Agreement,
|
||||
StrongLoop grants to Customer, during the Subscription Term (as defined in
|
||||
Section 7.1 (Term and Termination) of this Agreement, a limited, non-exclusive,
|
||||
non-transferable right and license, to install and use the StrongLoop Suite
|
||||
software (the "Software") and the documentation made available electronically as
|
||||
part of the Software (the "Documentation"), either of which may be modified
|
||||
during the Term (as defined in Section 7.1 below), solely for development,
|
||||
production and commercial purposes so long as Customer is using the Software to
|
||||
run only one process on a given operating system at a time. This Agreement,
|
||||
including but not limited to the license and restrictions contained herein,
|
||||
apply to Customer regardless of whether Customer accesses the Software via
|
||||
download from the StrongLoop Site or through a third-party website or service,
|
||||
even if Customer acquired the Software prior to agreeing to this Agreement.
|
||||
1.3 License Restrictions. Customer shall not itself, or through any parent,
|
||||
subsidiary, affiliate, agent or other third party:
|
||||
1.3.1 sell, lease, license, distribute, sublicense or otherwise transfer
|
||||
in whole or in part, any Software or the Documentation to a third party;
|
||||
or
|
||||
1.3.2 decompile, disassemble, translate, reverse engineer or otherwise
|
||||
attempt to derive source code from the Software, in whole or in part, nor
|
||||
shall Customer use any mechanical, electronic or other method to trace,
|
||||
decompile, disassemble, or identify the source code of the Software or
|
||||
encourage others to do so, except to the limited extent, if any, that
|
||||
applicable law permits such acts notwithstanding any contractual
|
||||
prohibitions, provided, however, before Customer exercises any rights that
|
||||
Customer believes to be entitled to based on mandatory law, Customer shall
|
||||
provide StrongLoop with thirty (30) days prior written notice and provide
|
||||
all reasonably requested information to allow StrongLoop to assess
|
||||
Customer's claim and, at StrongLoop's sole discretion, to provide
|
||||
alternatives that reduce any adverse impact on StrongLoop's intellectual
|
||||
property or other rights; or
|
||||
1.3.3 allow access or permit use of the Software by any users other than
|
||||
Customer's employees or authorized third-party contractors who are
|
||||
providing services to Customer and agree in writing to abide by the terms
|
||||
of this Agreement, provided further that Customer shall be liable for any
|
||||
failure by such employees and third-party contractors to comply with the
|
||||
terms of this Agreement and no usage restrictions, if any, shall be
|
||||
exceeded; or
|
||||
1.3.4 create, develop, license, install, use, or deploy any third party
|
||||
software or services to circumvent or provide access, permissions or
|
||||
rights which violate the license keys embedded within the Software; or
|
||||
1.3.5 modify or create derivative works based upon the Software or
|
||||
Documentation; or disclose the results of any benchmark test of the
|
||||
Software to any third party without StrongLoop's prior written approval;
|
||||
or
|
||||
1.3.6 change any proprietary rights notices which appear in the Software
|
||||
or Documentation; or
|
||||
1.3.7 use the Software as part of a time sharing or service bureau
|
||||
purposes or in any other resale capacity.
|
||||
1.4 Third-Party Software. The Software may include individual certain software
|
||||
that is owned by third parties, including individual open source software
|
||||
components (the "Third-Party Software"), each of which has its own copyright and
|
||||
its own applicable license conditions. Such third-party software is licensed to
|
||||
Customer under the terms of the applicable third-party licenses and/or copyright
|
||||
notices that can be found in the LICENSES file, the Documentation or other
|
||||
materials accompanying the Software, except that Sections 5 (Warranty
|
||||
Disclaimer) and 6 (Limitation of Liability) also govern Customer's use of the
|
||||
third-party software. Customer agrees to comply with the terms and conditions
|
||||
of the relevant third-party software licenses.
|
||||
2. Support Services. StrongLoop has no obligation to provide any support for
|
||||
the Software other than the support services specifically described on the
|
||||
StrongLoop Site for the Subscription level procured by Customer. However,
|
||||
StrongLoop has endeavored to establish a community of users of the Software who
|
||||
have provided their own feedback, hints and advice regarding their experiences
|
||||
in using the Software. You can find that community and user feedback on the
|
||||
StrongLoop Site. The use of any information, content or other materials from,
|
||||
contained in or on the StrongLoop Site are subject to the StrongLoop website
|
||||
terms of use located here http://www.strongloop.com/terms-of-service.
|
||||
3. Confidentiality. For purposes of this Agreement, "Confidential Information"
|
||||
means any and all information or proprietary materials (in every form and media)
|
||||
not generally known in the relevant trade or industry and which has been or is
|
||||
hereafter disclosed or made available by StrongLoop to Customer in connection
|
||||
with the transactions contemplated under this Agreement, including (i) all trade
|
||||
secrets, (ii) existing or contemplated Software, services, designs, technology,
|
||||
processes, technical data, engineering, techniques, methodologies and concepts
|
||||
and any related information, and (iii) information relating to business plans,
|
||||
sales or marketing methods and customer lists or requirements. For a period of
|
||||
five (5) years from the date of disclosure of the applicable Confidential
|
||||
Information, Customer shall (i) hold the Confidential Information in trust and
|
||||
confidence and avoid the disclosure or release thereof to any other person or
|
||||
entity by using the same degree of care as it uses to avoid unauthorized use,
|
||||
disclosure, or dissemination of its own Confidential Information of a similar
|
||||
nature, but not less than reasonable care, and (ii) not use the Confidential
|
||||
Information for any purpose whatsoever except as expressly contemplated under
|
||||
this Agreement; provided that, to the extent the Confidential Information
|
||||
constitutes a trade secret under law, Customer agrees to protect such
|
||||
information for so long as it qualifies as a trade secret under applicable law.
|
||||
Customer shall disclose the Confidential Information only to those of its
|
||||
employees and contractors having a need to know such Confidential Information
|
||||
and shall take all reasonable precautions to ensure that such employees and
|
||||
contractors comply with the provisions of this Section. The obligations of
|
||||
Customer under this Section shall not apply to information that Customer can
|
||||
demonstrate (i) was in its possession at the time of disclosure and without
|
||||
restriction as to confidentiality, (ii) at the time of disclosure is generally
|
||||
available to the public or after disclosure becomes generally available to the
|
||||
public through no breach of agreement or other wrongful act by Customer, (iii)
|
||||
has been received from a third party without restriction on disclosure and
|
||||
without breach of agreement by Customer, or (iv) is independently developed by
|
||||
Customer without regard to the Confidential Information. In addition, Customer
|
||||
may disclose Confidential Information as required to comply with binding orders
|
||||
of governmental entities that have jurisdiction over it; provided that Customer
|
||||
gives StrongLoop reasonable written notice to allow StrongLoop to seek a
|
||||
protective order or other appropriate remedy, discloses only such Confidential
|
||||
Information as is required by the governmental entity, and uses commercially
|
||||
reasonable efforts to obtain confidential treatment for any Confidential
|
||||
Information disclosed. Notwithstanding the above, Customer agrees that
|
||||
StrongLoop, its employees and agents shall be free to use and employ their
|
||||
general skills, know-how, and expertise, and to use, disclose, and employ any
|
||||
generalized ideas, concepts, know-how, methods, techniques or skills gained or
|
||||
learned during the Term or thereafter.
|
||||
4. Ownership. StrongLoop shall retain all intellectual property and proprietary
|
||||
rights in the Software, Documentation, and related works, including but not
|
||||
limited to any derivative work of the foregoing and StrongLoop's licensors shall
|
||||
retain all intellectual property and proprietary rights in any Third-Party
|
||||
Software that may be provided with or as a part of the Software. Customer shall
|
||||
do nothing inconsistent with StrongLoop's or its licensors' title to the
|
||||
Software and the intellectual property rights embodied therein, including, but
|
||||
not limited to, transferring, loaning, selling, assigning, pledging, or
|
||||
otherwise disposing, encumbering, or suffering a lien or encumbrance upon or
|
||||
against any interest in the Software. The Software (including any Third-Party
|
||||
Software) contain copyrighted material, trade secrets and other proprietary
|
||||
material of StrongLoop and/or its licensors.
|
||||
5. Warranty Disclaimer. THE SOFTWARE (INCLUDING ANY THIRD-PARTY SOFTWARE) AND
|
||||
DOCUMENTATION MADE AVAILABLE TO CUSTOMER ARE PROVIDED "AS-IS" AND STRONGLOOP,
|
||||
ON BEHALF OF ITSELF AND ITS LICENSORS, EXPRESSLY DISCLAIMS ALL WARRANTIES OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE,
|
||||
PERFORMANCE, AND ACCURACY AND ANY IMPLIED WARRANTIES ARISING FROM STATUTE,
|
||||
COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE. STRONGLOOP DOES
|
||||
NOT WARRANT THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR
|
||||
ERROR-FREE, THAT DEFECTS IN THE SOFTWARE WILL BE CORRECTED OR THAT THE SOFTWARE
|
||||
WILL PROVIDE OR ENSURE ANY PARTICULAR RESULTS OR OUTCOME. NO ORAL OR WRITTEN
|
||||
INFORMATION OR ADVICE GIVEN BY STRONGLOOP OR ITS AUTHORIZED REPRESENTATIVES
|
||||
SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF THIS WARRANTY.
|
||||
STRONGLOOP IS NOT OBLIGATED TO PROVIDE CUSTOMER WITH UPGRADES TO THE SOFTWARE,
|
||||
BUT MAY ELECT TO DO SO IN ITS SOLE DISCRETION. SOME JURISDICTIONS DO NOT ALLOW
|
||||
THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO
|
||||
CUSTOMER.WITHOUT LIMITING THE GENERALITY OF THE FOREGOING DISCLAIMER, THE
|
||||
SOFTWARE AND DOCUMENTATION ARE NOT DESIGNED, MANUFACTURED OR INTENDED FOR USE IN
|
||||
THE PLANNING, CONSTRUCTION, MAINTENANCE, CONTROL, OR DIRECT OPERATION OF NUCLEAR
|
||||
FACILITIES, AIRCRAFT NAVIGATION, CONTROL OR COMMUNICATION SYSTEMS, WEAPONS
|
||||
SYSTEMS, OR DIRECT LIFE SUPPORT SYSTEMS.
|
||||
6. Limitation of Liability.
|
||||
6.1 Exclusion of Liability. IN NO EVENT WILL STRONGLOOP OR ITS LICENSORS
|
||||
BE LIABLE UNDER THIS AGREEMENT FOR ANY INDIRECT, RELIANCE, PUNITIVE,
|
||||
CONSEQUENTIAL, SPECIAL, EXEMPLARY, OR INCIDENTAL DAMAGES OF ANY KIND AND
|
||||
HOWEVER CAUSED (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF
|
||||
BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION AND
|
||||
THE LIKE), EVEN IF STRONGLOOP HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES. CUSTOMER BEARS FULL RESPONSIBILITY FOR USE OF THE SOFTWARE AND
|
||||
THE SUBSCRIPTION AND STRONGLOOP DOES NOT GUARANTEE THAT THE USE OF THE
|
||||
SOFTWARE AND SUBSCRIPTION WILL ENSURE THAT CUSTOMER'S NETWORK WILL BE
|
||||
AVAILABLE, SECURE, MONITORED OR PROTECTED AGAINST ANY DOWNTIME, DENIAL OF
|
||||
SERVICE ATTACKS, SECUITY BREACHES, HACKERS AND THE LIKE. IN NO EVENT WILL
|
||||
STRONGLOOP'S CUMULATIVE LIABILITY FOR ANY DAMAGES, LOSSES AND CAUSES OF
|
||||
ACTION (WHETHER IN CONTRACT, TORT, INCLUDING NEGLIGENCE, OR OTHERWISE)
|
||||
ARISING OUT OF OR RELATED TO THIS AGREEMENT EXCEED THE GREATER OF ONE
|
||||
HUNDRED DOLLARS (US$100) OR THE TOTAL SUBSCRIPTION FEES PAID BY CUSTOMER
|
||||
TO STRONGLOOP IN THE TWELVE (12) MONTHS PRECEDING THE DATE THE CLAIM
|
||||
ARISES.
|
||||
6.2 Limitation of Damages. IN NO EVENT WILL STRONGLOOP'S LICENSORS HAVE
|
||||
ANY LIABILITY FOR ANY CLAIM ARISING IN CONNECTION WITH THIS AGREEMENT.
|
||||
THE PROVISIONS OF THIS SECTION 6 ALLOCATE RISKS UNDER THIS AGREEMENT
|
||||
BETWEEN CUSTOMER, STRONGLOOP AND STRONGLOOP'S SUPPLIERS. THE FOREGOING
|
||||
LIMITATIONS, EXCLUSIONS AND DISCLAIMERS APPLY TO THE MAXIMUM EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, EVEN IF ANY REMEDY FAILS IN ITS ESSENTIAL
|
||||
PURPOSE.
|
||||
6.3 Failure of Essential Purpose. THE PARTIES AGREE THAT THESE
|
||||
LIMITATIONS SHALL APPLY EVEN IF THIS AGREEMENT OR ANY LIMITED REMEDY
|
||||
SPECIFIED HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE.
|
||||
6.4 Allocation of Risk. The sections on limitation of liability and
|
||||
disclaimer of warranties allocate the risks in the Agreement between the
|
||||
parties. This allocation is an essential element of the basis of the
|
||||
bargain between the parties.
|
||||
7. Term and Termination.
|
||||
7.1 This Agreement shall commence on the Effective Date and continue for so long
|
||||
as Customer has a valid Subscription and is current on the payment of any
|
||||
Subscription Fees required to be paid for that Subscription (the "Subscription
|
||||
Term"). Either party may terminate this Agreement immediately upon written
|
||||
notice to the other party, and the Subscription and licenses granted hereunder
|
||||
automatically terminate upon the termination of this Agreement. This Agreement
|
||||
will terminate immediately without notice from StrongLoop if Customer fails to
|
||||
comply with or otherwise breaches any provision of this Agreement.
|
||||
7.2 All Sections other than Section 1.1 (Subscriptions) and 1.2 (Licenses) shall
|
||||
survive the expiration or termination of this Agreement.
|
||||
8. Subscription Fees and Payments. StrongLoop, Customer agrees to pay
|
||||
StrongLoop the Subscription Fees as described on the StrongLoop Site for the
|
||||
Subscription purchased unless a different amount has been agreed to in a
|
||||
separate agreement between Customer and StrongLoop. In addition, Customer shall
|
||||
pay all sales, use, value added, withholding, excise taxes and other tax, duty,
|
||||
custom and similar fees levied upon the delivery or use of the Software and the
|
||||
Subscriptions described in this Agreement. Fees shall be invoiced in full upon
|
||||
StrongLoop's acceptance of Customer's purchase order for the Subscription. All
|
||||
invoices shall be paid in US dollars and are due upon receipt and shall be paid
|
||||
within thirty (30) days. Payments shall be made without right of set-off or
|
||||
chargeback. If Customer does not pay the invoices when due, StrongLoop may
|
||||
charge interest at one percent (1%) per month or the highest rate permitted by
|
||||
law, whichever is lower, on the unpaid balance from the original due date. If
|
||||
Customer fails to pay fees in accordance with this Section, StrongLoop may
|
||||
suspend fulfilling its obligations under this Agreement (including but not
|
||||
limited to suspending the services under the Subscription) until payment is
|
||||
received by StrongLoop. If any applicable law requires Customer to withhold
|
||||
amounts from any payments to StrongLoop under this Agreement, (a) Customer shall
|
||||
effect such withholding, remit such amounts to the appropriate taxing
|
||||
authorities and promptly furnish StrongLoop with tax receipts evidencing the
|
||||
payments of such amounts and (b) the sum payable by Customer upon which the
|
||||
deduction or withholding is based shall be increased to the extent necessary to
|
||||
ensure that, after such deduction or withholding, StrongLoop receives and
|
||||
retains, free from liability for such deduction or withholding, a net amount
|
||||
equal to the amount StrongLoop would have received and retained absent the
|
||||
required deduction or withholding.
|
||||
9. General.
|
||||
9.1 Compliance with Laws. Customer shall abide by all local, state, federal and
|
||||
international laws, rules, regulations and orders applying to Customer's use of
|
||||
the Software, including, without limitation, the laws and regulations of the
|
||||
United States that may restrict the export and re-export of certain commodities
|
||||
and technical data of United States origin, including the Software. Customer
|
||||
agrees that it will not export or re-export the Software without the appropriate
|
||||
United States or foreign government licenses.
|
||||
9.2 Entire Agreement. This Agreement constitutes the entire agreement between
|
||||
the parties concerning the subject matter hereof. This Agreement supersedes all
|
||||
prior or contemporaneous discussions, proposals and agreements between the
|
||||
parties relating to the subject matter hereof. No amendment, modification or
|
||||
waiver of any provision of this Agreement shall be effective unless in writing
|
||||
and signed by both parties. Any additional or different terms on any purchase
|
||||
orders issued by Customer to StrongLoop shall not be binding on either party,
|
||||
are hereby rejected by StrongLoop and void.
|
||||
9.3 Severability. If any provision of this Agreement is held to be invalid or
|
||||
unenforceable, the remaining portions shall remain in full force and effect and
|
||||
such provision shall be enforced to the maximum extent possible so as to effect
|
||||
the intent of the parties and shall be reformed to the extent necessary to make
|
||||
such provision valid and enforceable.
|
||||
9.4 Waiver. No waiver of rights by either party may be implied from any actions
|
||||
or failures to enforce rights under this Agreement.
|
||||
9.5 Force Majeure. Neither party shall be liable to the other for any delay or
|
||||
failure to perform due to causes beyond its reasonable control (excluding
|
||||
payment of monies due).
|
||||
9.6 No Third Party Beneficiaries. Unless otherwise specifically stated, the
|
||||
terms of this Agreement are intended to be and are solely for the benefit of
|
||||
StrongLoop and Customer and do not create any right in favor of any third party.
|
||||
9.7 Governing Law and Jurisdiction. This Agreement shall be governed by the
|
||||
laws of the State of California, without reference to the principles of
|
||||
conflicts of law. The provisions of the Uniform Computerized Information
|
||||
Transaction Act and United Nations Convention on Contracts for the International
|
||||
Sale of Goods shall not apply to this Agreement. The parties shall attempt to
|
||||
resolve any dispute related to this Agreement informally, initially through
|
||||
their respective management, and then by non-binding mediation in San Francisco
|
||||
County, California. Any litigation related to this Agreement shall be brought
|
||||
in the state or federal courts located in San Francisco County, California, and
|
||||
only in those courts and each party irrevocably waives any objections to such
|
||||
venue.
|
||||
9.8 Notices. All notices must be in writing and shall be effective three (3)
|
||||
days after the date sent to the other party's headquarters, Attention Chief
|
||||
Financial Officer.
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# LoopBack
|
||||
|
||||
For a quick introduction and overview, see http://loopback.io/.
|
||||
|
||||
## Documentation
|
||||
|
||||
[See the full documentation](http://docs.strongloop.com/display/DOC/LoopBack).
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"lib/models/application.js",
|
||||
"lib/models/email.js",
|
||||
"lib/models/model.js",
|
||||
"lib/models/data-model.js",
|
||||
"lib/models/role.js",
|
||||
"lib/models/user.js",
|
||||
"lib/models/change.js",
|
||||
|
|
|
@ -179,9 +179,9 @@ User.afterRemote('**', function (ctx, user, next) {
|
|||
|
||||
Remote hooks are provided with a Context `ctx` object which contains transport specific data (eg. for http: `req` and `res`). The `ctx` object also has a set of consistent apis across transports.
|
||||
|
||||
#### ctx.user
|
||||
#### ctx.req.accessToken
|
||||
|
||||
A `Model` representing the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user.
|
||||
The `accessToken` of the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user (or other principal).
|
||||
|
||||
#### ctx.result
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ var oracle = loopback.createDataSource({
|
|||
User.attachTo(oracle);
|
||||
```
|
||||
|
||||
**Note:** until a model is attached to a data source it will **not** have any **attached methods**.
|
||||
NOTE: until a model is attached to a data source it will not have any attached methods.
|
||||
|
||||
### Properties
|
||||
|
||||
|
@ -257,7 +257,7 @@ User.findById(23, function(err, user) {
|
|||
Find a single instance that matches the given where expression.
|
||||
|
||||
```js
|
||||
User.findOne({id: 23}, function(err, user) {
|
||||
User.findOne({where: {id: 23}}, function(err, user) {
|
||||
console.info(user.id); // 23
|
||||
});
|
||||
```
|
||||
|
|
|
@ -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.Memory = require('./lib/connectors/memory');
|
||||
loopback.Mail = require('./lib/connectors/mail');
|
||||
loopback.Remote = require('./lib/connectors/remote');
|
||||
|
||||
/**
|
||||
* Types
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
var DataSource = require('loopback-datasource-juggler').DataSource
|
||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
||||
, compat = require('./compat')
|
||||
, assert = require('assert')
|
||||
, fs = require('fs')
|
||||
, _ = require('underscore')
|
||||
, RemoteObjects = require('strong-remoting')
|
||||
, swagger = require('strong-remoting/ext/swagger')
|
||||
, stringUtils = require('underscore.string')
|
||||
|
@ -46,8 +48,7 @@ var app = exports = module.exports = {};
|
|||
/**
|
||||
* Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
|
||||
*
|
||||
* **NOTE:** Calling `app.remotes()` multiple times will only ever return a
|
||||
* single set of remote objects.
|
||||
* **NOTE:** Calling `app.remotes()` more than once returns only a single set of remote objects.
|
||||
* @returns {RemoteObjects}
|
||||
*/
|
||||
|
||||
|
@ -55,7 +56,13 @@ app.remotes = function () {
|
|||
if(this._remotes) {
|
||||
return this._remotes;
|
||||
} else {
|
||||
return (this._remotes = RemoteObjects.create());
|
||||
var options = {};
|
||||
|
||||
if(this.get) {
|
||||
options = this.get('remoting');
|
||||
}
|
||||
|
||||
return (this._remotes = RemoteObjects.create(options));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,11 +92,13 @@ app.disuse = function (route) {
|
|||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {String} modelName The name of the model to define
|
||||
* @options {Object} config The model's configuration
|
||||
* @property {String} dataSource The `DataSource` to attach the model to
|
||||
* @property {Object} [options] an object containing `Model` options
|
||||
* @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language)
|
||||
* @param {String} modelName The name of the model to define.
|
||||
* @options {Object} config The model's configuration.
|
||||
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
|
||||
* @property {Object} [options] an object containing `Model` options.
|
||||
* @property {ACL[]} [options.acls] an array of `ACL` definitions.
|
||||
* @property {String[]} [options.hidden] **experimental** an array of properties to hide when accessed remotely.
|
||||
* @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language).
|
||||
* @end
|
||||
* @returns {ModelConstructor} the model class
|
||||
*/
|
||||
|
@ -97,9 +106,11 @@ app.disuse = function (route) {
|
|||
app.model = function (Model, config) {
|
||||
if(arguments.length === 1) {
|
||||
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
|
||||
assert(Model.pluralModelName, 'Model must have a "pluralModelName" property');
|
||||
this.remotes().exports[Model.pluralModelName] = Model;
|
||||
assert(Model.modelName, 'Model must have a "modelName" property');
|
||||
var remotingClassName = compat.getClassNameForRemoting(Model);
|
||||
this.remotes().exports[remotingClassName] = Model;
|
||||
this.models().push(Model);
|
||||
clearHandlerCache(this);
|
||||
Model.shared = true;
|
||||
Model.app = this;
|
||||
Model.emit('attached', this);
|
||||
|
@ -122,14 +133,11 @@ app.model = function (Model, config) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the models exported by the app. Only models defined using `app.model()`
|
||||
* will show up in this list.
|
||||
* Get the models exported by the app. Returns only models defined using `app.model()`
|
||||
*
|
||||
* There are two ways how to access models.
|
||||
* There are two ways to access models:
|
||||
*
|
||||
* **1. A list of all models**
|
||||
*
|
||||
* Call `app.models()` to get a list of all models.
|
||||
* 1. Call `app.models()` to get a list of all models.
|
||||
*
|
||||
* ```js
|
||||
* var models = app.models();
|
||||
|
@ -139,12 +147,11 @@ app.model = function (Model, config) {
|
|||
* });
|
||||
* ```
|
||||
*
|
||||
* **2. By model name**
|
||||
*
|
||||
* **2. Use `app.model` to access a model by name.
|
||||
* `app.model` has properties for all defined models.
|
||||
*
|
||||
* In the following example the `Product` and `CustomerReceipt` models are
|
||||
* accessed using the `models` object.
|
||||
* The following example illustrates accessing the `Product` and `CustomerReceipt` models
|
||||
* using the `models` object.
|
||||
*
|
||||
* ```js
|
||||
* var loopback = require('loopback');
|
||||
|
@ -171,7 +178,7 @@ app.model = function (Model, config) {
|
|||
* var customerReceipt = app.models.customerReceipt;
|
||||
* ```
|
||||
*
|
||||
* @returns {Array} a list of model classes
|
||||
* @returns {Array} Array of model classes.
|
||||
*/
|
||||
|
||||
app.models = function () {
|
||||
|
@ -193,6 +200,7 @@ app.dataSource = function (name, config) {
|
|||
|
||||
/**
|
||||
* Get all remote objects.
|
||||
* @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
|
||||
*/
|
||||
|
||||
app.remoteObjects = function () {
|
||||
|
@ -203,26 +211,17 @@ app.remoteObjects = function () {
|
|||
models.forEach(function (ModelCtor) {
|
||||
// only add shared models
|
||||
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
|
||||
result[ModelCtor.pluralModelName] = ModelCtor;
|
||||
result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the apps set of remote objects.
|
||||
*/
|
||||
|
||||
app.remotes = function () {
|
||||
return this._remotes || (this._remotes = RemoteObjects.create());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable swagger REST API documentation.
|
||||
*
|
||||
* > Note: This method is deprecated, use the extension
|
||||
* [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
|
||||
* **Note**: This method is deprecated. Use [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
|
||||
*
|
||||
* **Options**
|
||||
*
|
||||
|
@ -282,11 +281,16 @@ app.enableAuth = function() {
|
|||
var modelId = modelInstance && modelInstance.id || req.param('id');
|
||||
|
||||
if(Model.checkAccess) {
|
||||
// Pause the request before checking access
|
||||
// See https://github.com/strongloop/loopback-storage-service/issues/7
|
||||
req.pause();
|
||||
Model.checkAccess(
|
||||
req.accessToken,
|
||||
modelId,
|
||||
method.name,
|
||||
function(err, allowed) {
|
||||
// Emit any cached data events that fired while checking access.
|
||||
req.resume();
|
||||
if(err) {
|
||||
console.log(err);
|
||||
next(err);
|
||||
|
@ -303,34 +307,37 @@ app.enableAuth = function() {
|
|||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.isAuthEnabled = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize an application from an options object or a set of JSON and JavaScript files.
|
||||
*
|
||||
* **What happens during an app _boot_?**
|
||||
* This function takes an optional argument that is either a string or an object.
|
||||
*
|
||||
* 1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory
|
||||
* 2. **Models** are created from an `options.models` object or `models.json` in the current directory
|
||||
* 3. Any JavaScript files in the `./models` directory are loaded with `require()`.
|
||||
* 4. Any JavaScript files in the `./boot` directory are loaded with `require()`.
|
||||
* If the argument is a string, then it sets the application root directory based on the string value. Then it:
|
||||
* 1. Creates DataSources from the `datasources.json` file in the application root directory.
|
||||
* 2. Creates Models from the `models.json` file in the application root directory.
|
||||
*
|
||||
* **Options**
|
||||
*
|
||||
* - `cwd` - _optional_ - the directory to use when loading JSON and JavaScript files
|
||||
* - `models` - _optional_ - an object containing `Model` definitions
|
||||
* - `dataSources` - _optional_ - an object containing `DataSource` definitions
|
||||
* If the argument is an object, then it looks for `model`, `dataSources`, and `appRootDir` properties of the object.
|
||||
* If the object has no `appRootDir` property then it sets the current working directory as the application root directory.
|
||||
* Then it:
|
||||
* 1. Creates DataSources from the `options.dataSources` object.
|
||||
* 2. Creates Models from the `options.models` object.
|
||||
*
|
||||
* > **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
|
||||
* > files may result
|
||||
* > in models being **undefined** due to race conditions. To avoid this when
|
||||
* > using `app.boot()`
|
||||
* > make sure all models are passed as part of the `models` definition.
|
||||
* In both cases, the function loads JavaScript files in the `/models` and `/boot` subdirectories of the application root directory with `require()`.
|
||||
*
|
||||
* **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
|
||||
* files may result in models being **undefined** due to race conditions.
|
||||
* To avoid this when using `app.boot()` make sure all models are passed as part of the `models` definition.
|
||||
*
|
||||
* Throws an error if the config object is not valid or if boot fails.
|
||||
*
|
||||
* <a name="model-definition"></a>
|
||||
* **Model Definitions**
|
||||
*
|
||||
* The following is an example of an object containing two `Model` definitions: "location" and "inventory".
|
||||
* The following is example JSON for two `Model` definitions: "dealership" and "location".
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
|
@ -375,20 +382,13 @@ app.enableAuth = function() {
|
|||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Model definition properties**
|
||||
*
|
||||
* - `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to
|
||||
* - `options` - _optional_ - an object containing `Model` options
|
||||
* - `properties` _optional_ - an object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language)
|
||||
*
|
||||
* **DataSource definition properties**
|
||||
*
|
||||
* - `connector` - **required** - the name of the [connector](#working-with-data-sources-and-connectors)
|
||||
* @options {String|Object} options Boot options; If String, this is the application root directory; if object, has below properties.
|
||||
* @property {String} appRootDir Directory to use when loading JSON and JavaScript files (optional). Defaults to the current directory (`process.cwd()`).
|
||||
* @property {Object} models Object containing `Model` definitions (optional).
|
||||
* @property {Object} dataSources Object containing `DataSource` definitions (optional).
|
||||
* @end
|
||||
*
|
||||
* @header app.boot([options])
|
||||
* @throws {Error} If config is not valid
|
||||
* @throws {Error} If boot fails
|
||||
*/
|
||||
|
||||
app.boot = function(options) {
|
||||
|
@ -427,15 +427,16 @@ app.boot = function(options) {
|
|||
process.env.npm_package_config_host ||
|
||||
app.get('host');
|
||||
|
||||
appConfig.port =
|
||||
process.env.npm_config_port ||
|
||||
process.env.OPENSHIFT_SLS_PORT ||
|
||||
process.env.OPENSHIFT_NODEJS_PORT ||
|
||||
process.env.PORT ||
|
||||
appConfig.port ||
|
||||
process.env.npm_package_config_port ||
|
||||
app.get('port') ||
|
||||
3000;
|
||||
appConfig.port = _.find([
|
||||
process.env.npm_config_port,
|
||||
process.env.OPENSHIFT_SLS_PORT,
|
||||
process.env.OPENSHIFT_NODEJS_PORT,
|
||||
process.env.PORT,
|
||||
appConfig.port,
|
||||
process.env.npm_package_config_port,
|
||||
app.get('port'),
|
||||
3000
|
||||
], _.isFinite);
|
||||
|
||||
appConfig.restApiRoot =
|
||||
appConfig.restApiRoot ||
|
||||
|
@ -540,7 +541,11 @@ function dataSourcesFromConfig(config) {
|
|||
|
||||
function modelFromConfig(name, config, app) {
|
||||
var ModelCtor = require('./loopback').createModel(name, config.properties, config.options);
|
||||
var dataSource = app.dataSources[config.dataSource];
|
||||
var dataSource = config.dataSource;
|
||||
|
||||
if(typeof dataSource === 'string') {
|
||||
dataSource = app.dataSources[dataSource];
|
||||
}
|
||||
|
||||
assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"');
|
||||
|
||||
|
@ -635,7 +640,12 @@ function tryReadConfig(cwd, fileName) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
function clearHandlerCache(app) {
|
||||
app._handlers = undefined;
|
||||
}
|
||||
|
||||
/*!
|
||||
* This function is now deprecated.
|
||||
* Install all express middleware required by LoopBack.
|
||||
*
|
||||
* It is possible to inject your own middleware by listening on one of the
|
||||
|
@ -784,7 +794,7 @@ app.installMiddleware = function() {
|
|||
* This way the port param contains always the real port number, even when
|
||||
* listen was called with port number 0.
|
||||
*
|
||||
* @param {Function=} cb If specified, the callback will be added as a listener
|
||||
* @param {Function} cb If specified, the callback is added as a listener
|
||||
* for the server's "listening" event.
|
||||
* @returns {http.Server} A node `http.Server` with this application configured
|
||||
* as the request handler.
|
||||
|
|
|
@ -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')
|
||||
, assert = require('assert')
|
||||
, debug = require('debug')
|
||||
, loopback = require('../loopback')
|
||||
, STUB = 'STUB';
|
||||
|
||||
/**
|
||||
|
@ -22,8 +23,10 @@ function MailConnector(settings) {
|
|||
var transports = settings.transports || [];
|
||||
this.transportsIndex = {};
|
||||
this.transports = [];
|
||||
|
||||
transports.forEach(this.setupTransport.bind(this));
|
||||
|
||||
if(loopback.isServer) {
|
||||
transports.forEach(this.setupTransport.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
MailConnector.initialize = function(dataSource, callback) {
|
||||
|
|
|
@ -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
|
||||
, path = require('path')
|
||||
, proto = require('./application')
|
||||
, utils = require('express/node_modules/connect').utils
|
||||
, DataSource = require('loopback-datasource-juggler').DataSource
|
||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
||||
, assert = require('assert')
|
||||
, i8n = require('inflection');
|
||||
, i8n = require('inflection')
|
||||
, merge = require('util')._extend
|
||||
, assert = require('assert');
|
||||
|
||||
/**
|
||||
* `loopback` is the main entry for LoopBack core module. It provides static
|
||||
* Main entry for LoopBack core module. It provides static properties and
|
||||
* methods to create models and data sources. The module itself is a function
|
||||
* that creates loopback `app`. For example,
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
* var loopback = require('loopback');
|
||||
* var app = loopback();
|
||||
* ```
|
||||
*
|
||||
* @class loopback
|
||||
* @header loopback
|
||||
*/
|
||||
|
||||
var loopback = exports = module.exports = createApplication;
|
||||
|
||||
/**
|
||||
* True if running in a browser environment; false otherwise.
|
||||
*/
|
||||
|
||||
loopback.isBrowser = typeof window !== 'undefined';
|
||||
|
||||
/**
|
||||
* True if running in a server environment; false otherwise.
|
||||
*/
|
||||
|
||||
loopback.isServer = !loopback.isBrowser;
|
||||
|
||||
/**
|
||||
* Framework version.
|
||||
*/
|
||||
|
@ -40,7 +54,12 @@ loopback.version = require('../package.json').version;
|
|||
|
||||
loopback.mime = express.mime;
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Compatibility layer, intentionally left undocumented.
|
||||
*/
|
||||
loopback.compat = require('./compat');
|
||||
|
||||
/*!
|
||||
* Create an loopback application.
|
||||
*
|
||||
* @return {Function}
|
||||
|
@ -50,7 +69,12 @@ loopback.mime = express.mime;
|
|||
function createApplication() {
|
||||
var app = express();
|
||||
|
||||
utils.merge(app, proto);
|
||||
merge(app, proto);
|
||||
|
||||
// Create a new instance of models registry per each app instance
|
||||
app.models = function() {
|
||||
return proto.models.apply(this, arguments);
|
||||
};
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -70,16 +94,20 @@ for (var key in express) {
|
|||
/*!
|
||||
* Expose additional loopback middleware
|
||||
* for example `loopback.configure` etc.
|
||||
*
|
||||
* ***only in node***
|
||||
*/
|
||||
|
||||
fs
|
||||
.readdirSync(path.join(__dirname, 'middleware'))
|
||||
.filter(function (file) {
|
||||
return file.match(/\.js$/);
|
||||
})
|
||||
.forEach(function (m) {
|
||||
loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m);
|
||||
});
|
||||
if (loopback.isServer) {
|
||||
fs
|
||||
.readdirSync(path.join(__dirname, 'middleware'))
|
||||
.filter(function (file) {
|
||||
return file.match(/\.js$/);
|
||||
})
|
||||
.forEach(function (m) {
|
||||
loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m);
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
* Error handler title
|
||||
|
@ -90,11 +118,10 @@ loopback.errorHandler.title = 'Loopback';
|
|||
/**
|
||||
* Create a data source with passing the provided options to the connector.
|
||||
*
|
||||
* @param {String} name (optional)
|
||||
* @param {Object} options
|
||||
*
|
||||
* - connector - an loopback connector
|
||||
* - other values - see the specified `connector` docs
|
||||
* @param {String} name Optional name.
|
||||
* @options {Object} Data Source options
|
||||
* @property {Object} connector LoopBack connector.
|
||||
* @property {*} Other properties See the relevant connector documentation.
|
||||
*/
|
||||
|
||||
loopback.createDataSource = function (name, options) {
|
||||
|
@ -115,7 +142,7 @@ loopback.createDataSource = function (name, options) {
|
|||
/**
|
||||
* Create a named vanilla JavaScript class constructor with an attached set of properties and options.
|
||||
*
|
||||
* @param {String} name - must be unique
|
||||
* @param {String} name Unique name.
|
||||
* @param {Object} properties
|
||||
* @param {Object} options (optional)
|
||||
*/
|
||||
|
@ -138,7 +165,7 @@ loopback.createModel = function (name, properties, options) {
|
|||
} catch(e) {}
|
||||
|
||||
return model;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a remote method to a model.
|
||||
|
@ -154,7 +181,7 @@ loopback.remoteMethod = function (fn, options) {
|
|||
});
|
||||
}
|
||||
fn.http = fn.http || {verb: 'get'};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a template helper.
|
||||
|
@ -170,7 +197,7 @@ loopback.template = function (file) {
|
|||
var templates = this._templates || (this._templates = {});
|
||||
var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
|
||||
return ejs.compile(str);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an in-memory data source. Use one if it already exists.
|
||||
|
@ -192,12 +219,12 @@ loopback.memory = function (name) {
|
|||
}
|
||||
|
||||
return memory;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Look up a model class by name from all models created by loopback.createModel()
|
||||
* @param {String} modelName The model name
|
||||
* @return {Model} The model class
|
||||
* @returns {Model} The model class
|
||||
*/
|
||||
loopback.getModel = function(modelName) {
|
||||
return loopback.Model.modelBuilder.models[modelName];
|
||||
|
@ -207,7 +234,7 @@ loopback.getModel = function(modelName) {
|
|||
* Look up a model class by the base model class. The method can be used by LoopBack
|
||||
* to find configured models in models.json over the base model.
|
||||
* @param {Model} The base model class
|
||||
* @return {Model} The subclass if found or the base class
|
||||
* @returns {Model} The subclass if found or the base class
|
||||
*/
|
||||
loopback.getModelByType = function(modelType) {
|
||||
assert(typeof modelType === 'function', 'The model type must be a constructor');
|
||||
|
@ -224,7 +251,7 @@ loopback.getModelByType = function(modelType) {
|
|||
* Set the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type
|
||||
* @param {Object|DataSource} dataSource The data source settings or instance
|
||||
* @return {DataSource} The data source instance
|
||||
* @returns {DataSource} The data source instance
|
||||
*/
|
||||
|
||||
loopback.setDefaultDataSourceForType = function(type, dataSource) {
|
||||
|
@ -236,17 +263,17 @@ loopback.setDefaultDataSourceForType = function(type, dataSource) {
|
|||
|
||||
defaultDataSources[type] = dataSource;
|
||||
return dataSource;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type
|
||||
* @return {DataSource} The data source instance
|
||||
* @returns {DataSource} The data source instance
|
||||
*/
|
||||
|
||||
loopback.getDefaultDataSourceForType = function(type) {
|
||||
return this.defaultDataSources && this.defaultDataSources[type];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach any model that does not have a dataSource to
|
||||
|
@ -265,7 +292,7 @@ loopback.autoAttach = function() {
|
|||
loopback.autoAttachModel(ModelCtor);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loopback.autoAttachModel = function(ModelCtor) {
|
||||
if(ModelCtor.autoAttach) {
|
||||
|
@ -277,13 +304,14 @@ loopback.autoAttachModel = function(ModelCtor) {
|
|||
|
||||
ModelCtor.attachTo(ds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
/*!
|
||||
* Built in models / services
|
||||
*/
|
||||
|
||||
loopback.Model = require('./models/model');
|
||||
loopback.DataModel = require('./models/data-model');
|
||||
loopback.Email = require('./models/email');
|
||||
loopback.User = require('./models/user');
|
||||
loopback.Application = require('./models/application');
|
||||
|
@ -304,6 +332,7 @@ var dataSourceTypes = {
|
|||
};
|
||||
|
||||
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
||||
loopback.DataModel.autoAttach = dataSourceTypes.DB;
|
||||
loopback.User.autoAttach = dataSourceTypes.DB;
|
||||
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Role.autoAttach = dataSourceTypes.DB;
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
|
||||
var loopback = require('../loopback');
|
||||
var RemoteObjects = require('strong-remoting');
|
||||
|
||||
/**
|
||||
* Export the middleware.
|
||||
|
@ -23,7 +22,7 @@ function rest() {
|
|||
if(req.url === '/routes') {
|
||||
res.send(handler.adapter.allRoutes());
|
||||
} else if(req.url === '/models') {
|
||||
return res.send(remotes.toJSON());
|
||||
return res.send(app.remotes().toJSON());
|
||||
} else {
|
||||
handler(req, res, next);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
|
||||
var loopback = require('../loopback');
|
||||
var RemoteObjects = require('strong-remoting');
|
||||
var assert = require('assert');
|
||||
|
||||
/*!
|
||||
|
|
|
@ -194,8 +194,8 @@ Principal.SCOPE = 'SCOPE';
|
|||
|
||||
/**
|
||||
* Compare if two principals are equal
|
||||
* @param p The other principal
|
||||
* @returns {boolean}
|
||||
* Returns true if argument principal is equal to this principal.
|
||||
* @param {Object} principal The other principal
|
||||
*/
|
||||
Principal.prototype.equals = function (p) {
|
||||
if (p instanceof Principal) {
|
||||
|
@ -205,11 +205,11 @@ Principal.prototype.equals = function (p) {
|
|||
};
|
||||
|
||||
/**
|
||||
* A request to access protected resources
|
||||
* A request to access protected resources.
|
||||
* @param {String} model The model name
|
||||
* @param {String} property
|
||||
* @param {String} accessType The access type
|
||||
* @param {String} permission The permission
|
||||
* @param {String} permission The requested permission
|
||||
* @returns {AccessRequest}
|
||||
* @class
|
||||
*/
|
||||
|
@ -217,10 +217,19 @@ function AccessRequest(model, property, accessType, permission) {
|
|||
if (!(this instanceof AccessRequest)) {
|
||||
return new AccessRequest(model, property, accessType);
|
||||
}
|
||||
this.model = model || AccessContext.ALL;
|
||||
this.property = property || AccessContext.ALL;
|
||||
this.accessType = accessType || AccessContext.ALL;
|
||||
this.permission = permission || AccessContext.DEFAULT;
|
||||
if (arguments.length === 1 && typeof model === 'object') {
|
||||
// The argument is an object that contains all required properties
|
||||
var obj = model || {};
|
||||
this.model = obj.model || AccessContext.ALL;
|
||||
this.property = obj.property || AccessContext.ALL;
|
||||
this.accessType = obj.accessType || AccessContext.ALL;
|
||||
this.permission = obj.permission || AccessContext.DEFAULT;
|
||||
} else {
|
||||
this.model = model || AccessContext.ALL;
|
||||
this.property = property || AccessContext.ALL;
|
||||
this.accessType = accessType || AccessContext.ALL;
|
||||
this.permission = permission || AccessContext.DEFAULT;
|
||||
}
|
||||
|
||||
if(debug.enabled) {
|
||||
debug('---AccessRequest---');
|
||||
|
|
|
@ -17,7 +17,7 @@ var Model = require('../loopback').Model
|
|||
*/
|
||||
|
||||
var properties = {
|
||||
id: {type: String, generated: true, id: 1},
|
||||
id: {type: String, id: true},
|
||||
ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds
|
||||
created: {type: Date, default: function() {
|
||||
return new Date();
|
||||
|
@ -53,7 +53,14 @@ var AccessToken = module.exports = Model.extend('AccessToken', properties, {
|
|||
property: 'create',
|
||||
permission: 'ALLOW'
|
||||
}
|
||||
]
|
||||
],
|
||||
relations: {
|
||||
user: {
|
||||
type: 'belongsTo',
|
||||
model: 'User',
|
||||
foreignKey: 'userId'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -137,6 +137,50 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
|||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Weigh against the principal type into 4 levels
|
||||
// - user level (explicitly allow/deny a given user)
|
||||
// - app level (explicitly allow/deny a given app)
|
||||
// - role level (role based authorization)
|
||||
// - other
|
||||
// user > app > role > ...
|
||||
score = score * 4;
|
||||
switch(rule.principalType) {
|
||||
case ACL.USER:
|
||||
score += 4;
|
||||
break;
|
||||
case ACL.APP:
|
||||
score += 3;
|
||||
break;
|
||||
case ACL.ROLE:
|
||||
score += 2;
|
||||
break;
|
||||
default:
|
||||
score +=1;
|
||||
}
|
||||
|
||||
// Weigh against the roles
|
||||
// everyone < authenticated/unauthenticated < related < owner < ...
|
||||
score = score * 8;
|
||||
if(rule.principalType === ACL.ROLE) {
|
||||
switch(rule.principalId) {
|
||||
case Role.OWNER:
|
||||
score += 4;
|
||||
break;
|
||||
case Role.RELATED:
|
||||
score += 3;
|
||||
break;
|
||||
case Role.AUTHENTICATED:
|
||||
case Role.UNAUTHENTICATED:
|
||||
score += 2;
|
||||
break;
|
||||
case Role.EVERYONE:
|
||||
score += 1;
|
||||
break;
|
||||
default:
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
score = score * 4;
|
||||
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
||||
return score;
|
||||
|
@ -149,10 +193,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
|||
* @returns {AccessRequest} result The effective ACL
|
||||
*/
|
||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||
if(!(req instanceof AccessRequest)) {
|
||||
req = new AccessRequest(req);
|
||||
}
|
||||
// Sort by the matching score in descending order
|
||||
acls = acls.sort(function (rule1, rule2) {
|
||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||
});
|
||||
if(debug.enabled) {
|
||||
debug('ACLs by order: %j', acls);
|
||||
}
|
||||
var permission = ACL.DEFAULT;
|
||||
var score = 0;
|
||||
for (var i = 0; i < acls.length; i++) {
|
||||
|
@ -351,6 +401,9 @@ ACL.checkAccess = function (context, callback) {
|
|||
inRoleTasks.push(function (done) {
|
||||
roleModel.isInRole(acl.principalId, context,
|
||||
function (err, inRole) {
|
||||
if(debug.enabled) {
|
||||
debug('In role %j: %j', acl.principalId, inRole);
|
||||
}
|
||||
if (!err && inRole) {
|
||||
effectiveACLs.push(acl);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ var PushNotificationSettingSchema = {
|
|||
* Data model for Application
|
||||
*/
|
||||
var ApplicationSchema = {
|
||||
id: {type: String, id: true, generated: true},
|
||||
id: {type: String, id: true},
|
||||
// Basic information
|
||||
name: {type: String, required: true}, // The name
|
||||
description: String, // The description
|
||||
|
@ -90,7 +90,7 @@ var ApplicationSchema = {
|
|||
modified: {type: Date, default: Date}
|
||||
};
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Application management functions
|
||||
*/
|
||||
|
||||
|
@ -98,12 +98,13 @@ var crypto = require('crypto');
|
|||
|
||||
function generateKey(hmacKey, algorithm, encoding) {
|
||||
hmacKey = hmacKey || 'loopback';
|
||||
algorithm = algorithm || 'sha256';
|
||||
encoding = encoding || 'base64';
|
||||
algorithm = algorithm || 'sha1';
|
||||
encoding = encoding || 'hex';
|
||||
var hmac = crypto.createHmac(algorithm, hmacKey);
|
||||
var buf = crypto.randomBytes(64);
|
||||
var buf = crypto.randomBytes(32);
|
||||
hmac.update(buf);
|
||||
return hmac.digest('base64');
|
||||
var key = hmac.digest(encoding);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,7 +122,7 @@ var Application = loopback.createModel('Application', ApplicationSchema);
|
|||
Application.beforeCreate = function (next) {
|
||||
var app = this;
|
||||
app.created = app.modified = new Date();
|
||||
app.id = generateKey('id', 'sha1');
|
||||
app.id = generateKey('id', 'md5');
|
||||
app.clientKey = generateKey('client');
|
||||
app.javaScriptKey = generateKey('javaScript');
|
||||
app.restApiKey = generateKey('restApi');
|
||||
|
@ -188,8 +189,7 @@ Application.resetKeys = function (appId, cb) {
|
|||
/**
|
||||
* Authenticate the application id and key.
|
||||
*
|
||||
* `matched` will be one of
|
||||
*
|
||||
* `matched` parameter is one of:
|
||||
* - clientKey
|
||||
* - javaScriptKey
|
||||
* - restApiKey
|
||||
|
@ -200,7 +200,7 @@ Application.resetKeys = function (appId, cb) {
|
|||
* @param {String} key
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} matched - The matching key
|
||||
* @param {String} matched The matching key
|
||||
*/
|
||||
Application.authenticate = function (appId, key, cb) {
|
||||
this.findById(appId, function (err, app) {
|
||||
|
@ -208,13 +208,18 @@ Application.authenticate = function (appId, key, cb) {
|
|||
cb && cb(err, null);
|
||||
return;
|
||||
}
|
||||
var matched = null;
|
||||
['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function (k) {
|
||||
if (app[k] === key) {
|
||||
matched = k;
|
||||
var result = null;
|
||||
var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
|
||||
for (var i = 0; i < keyNames.length; i++) {
|
||||
if (app[keyNames[i]] === key) {
|
||||
result = {
|
||||
application: app,
|
||||
keyType: keyNames[i]
|
||||
};
|
||||
break;
|
||||
}
|
||||
});
|
||||
cb && cb(null, matched);
|
||||
}
|
||||
cb && cb(null, result);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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**
|
||||
*
|
||||
* - `to` - **{ String }** **required**
|
||||
* - `from` - **{ String }** **required**
|
||||
* - `subject` - **{ String }** **required**
|
||||
* - `text` - **{ String }**
|
||||
* - `html` - **{ String }**
|
||||
* - `to` - String (required)
|
||||
* - `from` - String (required)
|
||||
* - `subject` - String (required)
|
||||
* - `text` - String
|
||||
* - `html` - String
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
|
@ -35,19 +35,24 @@ var Email = module.exports = Model.extend('Email', properties);
|
|||
*
|
||||
* Example Options:
|
||||
*
|
||||
* ```json
|
||||
* ```js
|
||||
* {
|
||||
* from: "Fred Foo ✔ <foo@blurdybloop.com>", // sender address
|
||||
* from: "Fred Foo <foo@blurdybloop.com>", // sender address
|
||||
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
|
||||
* subject: "Hello ✔", // Subject line
|
||||
* text: "Hello world ✔", // plaintext body
|
||||
* html: "<b>Hello world ✔</b>" // html body
|
||||
* subject: "Hello", // Subject line
|
||||
* text: "Hello world", // plaintext body
|
||||
* html: "<b>Hello world</b>" // html body
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See https://github.com/andris9/Nodemailer for other supported options.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @options {Object} options See below
|
||||
* @prop {String} from Senders's email address
|
||||
* @prop {String} to List of one or more recipient email addresses (comma-delimited)
|
||||
* @prop {String} subject Subject line
|
||||
* @prop {String} text Body text
|
||||
* @prop {String} html Body HTML (optional)
|
||||
* @param {Function} callback Called after the e-mail is sent or the sending failed
|
||||
*/
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Module Dependencies.
|
||||
*/
|
||||
var loopback = require('../loopback');
|
||||
var compat = require('../compat');
|
||||
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
|
||||
var modeler = new ModelBuilder();
|
||||
var async = require('async');
|
||||
|
@ -133,7 +134,8 @@ Model.setup = function () {
|
|||
var self = this;
|
||||
if(this.app) {
|
||||
var remotes = this.app.remotes();
|
||||
remotes.before(self.pluralModelName + '.' + name, function (ctx, next) {
|
||||
var className = compat.getClassNameForRemoting(self);
|
||||
remotes.before(className + '.' + name, function (ctx, next) {
|
||||
fn(ctx, ctx.result, next);
|
||||
});
|
||||
} else {
|
||||
|
@ -149,7 +151,8 @@ Model.setup = function () {
|
|||
var self = this;
|
||||
if(this.app) {
|
||||
var remotes = this.app.remotes();
|
||||
remotes.after(self.pluralModelName + '.' + name, function (ctx, next) {
|
||||
var className = compat.getClassNameForRemoting(self);
|
||||
remotes.after(className + '.' + name, function (ctx, next) {
|
||||
fn(ctx, ctx.result, next);
|
||||
});
|
||||
} else {
|
||||
|
@ -161,8 +164,9 @@ Model.setup = function () {
|
|||
};
|
||||
|
||||
// Map the prototype method to /:id with data in the body
|
||||
var idDesc = ModelCtor.modelName + ' id';
|
||||
ModelCtor.sharedCtor.accepts = [
|
||||
{arg: 'id', type: 'any', http: {source: 'path'}}
|
||||
{arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc}
|
||||
// {arg: 'instance', type: 'object', http: {source: 'body'}}
|
||||
];
|
||||
|
||||
|
@ -185,10 +189,20 @@ Model.setup = function () {
|
|||
/*!
|
||||
* Get the reference to ACL in a lazy fashion to avoid race condition in require
|
||||
*/
|
||||
var ACL = null;
|
||||
function getACL() {
|
||||
return ACL || (ACL = require('./acl').ACL);
|
||||
}
|
||||
var _aclModel = null;
|
||||
Model._ACL = function getACL(ACL) {
|
||||
if(ACL !== undefined) {
|
||||
// The function is used as a setter
|
||||
_aclModel = ACL;
|
||||
}
|
||||
if(_aclModel) {
|
||||
return _aclModel;
|
||||
}
|
||||
var aclModel = require('./acl').ACL;
|
||||
_aclModel = loopback.getModelByType(aclModel);
|
||||
return _aclModel;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check if the given access token can invoke the method
|
||||
|
@ -206,9 +220,9 @@ function getACL() {
|
|||
Model.checkAccess = function(token, modelId, method, callback) {
|
||||
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||
token = token || ANONYMOUS;
|
||||
var ACL = getACL();
|
||||
var aclModel = Model._ACL();
|
||||
var methodName = 'string' === typeof method? method: method && method.name;
|
||||
ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback);
|
||||
aclModel.checkAccessForToken(token, this.modelName, modelId, methodName, callback);
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -227,7 +241,7 @@ Model._getAccessTypeForMethod = function(method) {
|
|||
'method is a required argument and must be a RemoteMethod object'
|
||||
);
|
||||
|
||||
var ACL = getACL();
|
||||
var ACL = Model._ACL();
|
||||
|
||||
switch(method.name) {
|
||||
case'create':
|
||||
|
@ -259,6 +273,72 @@ Model._getAccessTypeForMethod = function(method) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Application` the Model is attached to.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Application} app
|
||||
* @end
|
||||
*/
|
||||
|
||||
Model.getApp = function(callback) {
|
||||
var Model = this;
|
||||
if(this.app) {
|
||||
callback(null, this.app);
|
||||
} else {
|
||||
Model.once('attached', function() {
|
||||
assert(Model.app);
|
||||
callback(null, Model.app);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Model's `RemoteObjects`.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {RemoteObjects} remoteObjects
|
||||
* @end
|
||||
*/
|
||||
|
||||
Model.remotes = function(callback) {
|
||||
this.getApp(function(err, app) {
|
||||
callback(null, app.remotes());
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
* Create a proxy function for invoking remote methods.
|
||||
*
|
||||
* @param {SharedMethod} sharedMethod
|
||||
*/
|
||||
|
||||
Model.createProxyMethod = function createProxyFunction(remoteMethod) {
|
||||
var Model = this;
|
||||
var scope = remoteMethod.isStatic ? Model : Model.prototype;
|
||||
var original = scope[remoteMethod.name];
|
||||
|
||||
var fn = scope[remoteMethod.name] = function proxy() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
|
||||
var callback;
|
||||
if(lastArgIsFunc) {
|
||||
callback = args.pop();
|
||||
}
|
||||
|
||||
Model.remotes(function(err, remotes) {
|
||||
remotes.invoke(remoteMethod.stringName, args, callback);
|
||||
});
|
||||
}
|
||||
|
||||
for(var key in original) {
|
||||
fn[key] = original[key];
|
||||
}
|
||||
fn._delegate = true;
|
||||
}
|
||||
|
||||
// setup the initial model
|
||||
Model.setup();
|
||||
|
||||
|
|
|
@ -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}
|
||||
};
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Map principals to roles
|
||||
*/
|
||||
var RoleMappingSchema = {
|
||||
|
@ -27,7 +27,10 @@ var RoleMappingSchema = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Map Roles to
|
||||
* The `RoleMapping` model extends from the built in `loopback.Model` type.
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, {
|
||||
|
@ -196,6 +199,21 @@ function isUserClass(modelClass) {
|
|||
modelClass.prototype instanceof loopback.User;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Check if two user ids matches
|
||||
* @param {*} id1
|
||||
* @param {*} id2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function matches(id1, id2) {
|
||||
if (id1 === undefined || id1 === null || id1 ===''
|
||||
|| id2 === undefined || id2 === null || id2 === '') {
|
||||
return false;
|
||||
}
|
||||
// The id can be a MongoDB ObjectID
|
||||
return id1 === id2 || id1.toString() === id2.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given userId is the owner the model instance
|
||||
* @param {Function} modelClass The model class
|
||||
|
@ -205,7 +223,7 @@ function isUserClass(modelClass) {
|
|||
*/
|
||||
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||
assert(modelClass, 'Model class is required');
|
||||
debug('isOwner(): %s %s %s', modelClass && modelClass.modelName, modelId, userId);
|
||||
debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId);
|
||||
// No userId is present
|
||||
if(!userId) {
|
||||
process.nextTick(function() {
|
||||
|
@ -217,19 +235,21 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
|||
// Is the modelClass User or a subclass of User?
|
||||
if(isUserClass(modelClass)) {
|
||||
process.nextTick(function() {
|
||||
callback(null, modelId == userId);
|
||||
callback(null, matches(modelId, userId));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modelClass.findById(modelId, function(err, inst) {
|
||||
if(err || !inst) {
|
||||
debug('Model not found for id %j', modelId);
|
||||
callback && callback(err, false);
|
||||
return;
|
||||
}
|
||||
debug('Model found: %j', inst);
|
||||
if(inst.userId || inst.owner) {
|
||||
callback && callback(null, (inst.userId || inst.owner) === userId);
|
||||
var ownerId = inst.userId || inst.owner;
|
||||
if(ownerId) {
|
||||
callback && callback(null, matches(ownerId, userId));
|
||||
return;
|
||||
} else {
|
||||
// Try to follow belongsTo
|
||||
|
@ -240,7 +260,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
|||
inst[r](function(err, user) {
|
||||
if(!err && user) {
|
||||
debug('User found: %j', user.id);
|
||||
callback && callback(null, user.id === userId);
|
||||
callback && callback(null, matches(user.id, userId));
|
||||
} else {
|
||||
callback && callback(err, false);
|
||||
}
|
||||
|
@ -248,6 +268,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId);
|
||||
callback && callback(null, false);
|
||||
}
|
||||
});
|
||||
|
@ -401,6 +422,9 @@ Role.getRoles = function (context, callback) {
|
|||
Object.keys(Role.resolvers).forEach(function (role) {
|
||||
inRoleTasks.push(function (done) {
|
||||
self.isInRole(role, context, function (err, inRole) {
|
||||
if(debug.enabled) {
|
||||
debug('In role %j: %j', role, inRole);
|
||||
}
|
||||
if (!err && inRole) {
|
||||
addRole(role);
|
||||
done();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
|
@ -18,7 +18,9 @@ var Model = require('../loopback').Model
|
|||
, ACL = require('./acl').ACL
|
||||
, assert = require('assert');
|
||||
|
||||
/**
|
||||
var debug = require('debug')('loopback:user');
|
||||
|
||||
/*!
|
||||
* Default User properties.
|
||||
*/
|
||||
|
||||
|
@ -48,11 +50,12 @@ var properties = {
|
|||
};
|
||||
|
||||
var options = {
|
||||
hidden: ['password'],
|
||||
acls: [
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: ACL.DENY,
|
||||
permission: ACL.DENY
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
|
@ -89,15 +92,28 @@ var options = {
|
|||
principalId: Role.OWNER,
|
||||
permission: ACL.ALLOW,
|
||||
property: "updateAttributes"
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: ACL.ALLOW,
|
||||
property: "confirm"
|
||||
}
|
||||
]
|
||||
],
|
||||
relations: {
|
||||
accessTokens: {
|
||||
type: 'hasMany',
|
||||
model: 'AccessToken',
|
||||
foreignKey: 'userId'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extends from the built in `loopback.Model` type.
|
||||
*
|
||||
* Default `User` ACLs.
|
||||
*
|
||||
*
|
||||
* - DENY EVERYONE `*`
|
||||
* - ALLOW EVERYONE `create`
|
||||
* - ALLOW OWNER `removeById`
|
||||
|
@ -127,40 +143,64 @@ var User = module.exports = Model.extend('User', properties, options);
|
|||
* @param {AccessToken} token
|
||||
*/
|
||||
|
||||
User.login = function (credentials, fn) {
|
||||
var UserCtor = this;
|
||||
User.login = function (credentials, include, fn) {
|
||||
if (typeof include === 'function') {
|
||||
fn = include;
|
||||
include = undefined;
|
||||
}
|
||||
|
||||
include = (include || '').toLowerCase();
|
||||
|
||||
var query = {};
|
||||
|
||||
if(credentials.email) {
|
||||
query.email = credentials.email;
|
||||
} else if(credentials.username) {
|
||||
query.username = credentials.username;
|
||||
} else {
|
||||
return fn(new Error('must provide username or email'));
|
||||
var err = new Error('username or email is required');
|
||||
err.statusCode = 400;
|
||||
return fn(err);
|
||||
}
|
||||
|
||||
|
||||
this.findOne({where: query}, function(err, user) {
|
||||
var defaultError = new Error('login failed');
|
||||
|
||||
defaultError.statusCode = 401;
|
||||
|
||||
if(err) {
|
||||
debug('An error is reported from User.findOne: %j', err);
|
||||
fn(defaultError);
|
||||
} else if(user) {
|
||||
user.hasPassword(credentials.password, function(err, isMatch) {
|
||||
if(err) {
|
||||
debug('An error is reported from User.hasPassword: %j', err);
|
||||
fn(defaultError);
|
||||
} else if(isMatch) {
|
||||
user.accessTokens.create({
|
||||
ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL)
|
||||
}, fn);
|
||||
}, function(err, token) {
|
||||
if (err) return fn(err);
|
||||
if (include === 'user') {
|
||||
// NOTE(bajtos) We can't set token.user here:
|
||||
// 1. token.user already exists, it's a function injected by
|
||||
// "AccessToken belongsTo User" relation
|
||||
// 2. ModelBaseClass.toJSON() ignores own properties, thus
|
||||
// the value won't be included in the HTTP response
|
||||
// See also loopback#161 and loopback#162
|
||||
token.__data.user = user;
|
||||
}
|
||||
fn(err, token);
|
||||
});
|
||||
} else {
|
||||
debug('The password is invalid for user %s', query.email || query.username);
|
||||
fn(defaultError);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug('No matching record is found for user %s', query.email || query.username);
|
||||
fn(defaultError);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout a user with the given accessToken id.
|
||||
|
@ -230,7 +270,7 @@ User.prototype.verify = function (options, fn) {
|
|||
assert(options.type === 'email', 'Unsupported verification type');
|
||||
assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property');
|
||||
assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property');
|
||||
|
||||
|
||||
options.redirect = options.redirect || '/';
|
||||
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
||||
options.user = this;
|
||||
|
@ -240,40 +280,44 @@ User.prototype.verify = function (options, fn) {
|
|||
options.protocol
|
||||
+ '://'
|
||||
+ options.host
|
||||
+ (User.sharedCtor.http.path || '/' + User.pluralModelName)
|
||||
+ User.confirm.http.path;
|
||||
|
||||
+ User.http.path
|
||||
+ User.confirm.http.path
|
||||
+ '?uid='
|
||||
+ options.user.id
|
||||
+ '&redirect='
|
||||
+ options.redirect;
|
||||
|
||||
|
||||
|
||||
// Email model
|
||||
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
||||
|
||||
|
||||
crypto.randomBytes(64, function(err, buf) {
|
||||
if(err) {
|
||||
fn(err);
|
||||
} else {
|
||||
user.verificationToken = buf.toString('base64');
|
||||
user.verificationToken = buf.toString('hex');
|
||||
user.save(function (err) {
|
||||
if(err) {
|
||||
fn(err);
|
||||
} else {
|
||||
sendEmail(user);
|
||||
sendEmail(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// TODO - support more verification types
|
||||
function sendEmail(user) {
|
||||
options.verifyHref += '?token=' + user.verificationToken;
|
||||
|
||||
options.verifyHref += '&token=' + user.verificationToken;
|
||||
|
||||
options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}';
|
||||
|
||||
|
||||
options.text = options.text.replace('{href}', options.verifyHref);
|
||||
|
||||
|
||||
var template = loopback.template(options.template);
|
||||
Email.send({
|
||||
to: options.to || user.email,
|
||||
from: options.from,
|
||||
subject: options.subject || 'Thanks for Registering',
|
||||
text: options.text,
|
||||
html: template(options)
|
||||
|
@ -372,7 +416,7 @@ User.setup = function () {
|
|||
// We need to call the base class's setup method
|
||||
Model.setup.call(this);
|
||||
var UserModel = this;
|
||||
|
||||
|
||||
// max ttl
|
||||
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
||||
this.settings.ttl = DEFAULT_TTL;
|
||||
|
@ -381,18 +425,27 @@ User.setup = function () {
|
|||
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
||||
this.$password = bcrypt.hashSync(plain, salt);
|
||||
}
|
||||
|
||||
|
||||
loopback.remoteMethod(
|
||||
UserModel.login,
|
||||
{
|
||||
accepts: [
|
||||
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}
|
||||
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
|
||||
{arg: 'include', type: 'string', http: {source: 'query' }, description:
|
||||
'Related objects to include in the response. ' +
|
||||
'See the description of return value for more details.'}
|
||||
],
|
||||
returns: {arg: 'accessToken', type: 'object', root: true},
|
||||
returns: {
|
||||
arg: 'accessToken', type: 'object', root: true, description:
|
||||
'The response body contains properties of the AccessToken created on login.\n' +
|
||||
'Depending on the value of `include` parameter, the body may contain ' +
|
||||
'additional properties:\n\n' +
|
||||
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
||||
},
|
||||
http: {verb: 'post'}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
loopback.remoteMethod(
|
||||
UserModel.logout,
|
||||
{
|
||||
|
@ -403,12 +456,15 @@ User.setup = function () {
|
|||
var tokenID = accessToken && accessToken.id;
|
||||
|
||||
return tokenID;
|
||||
}}
|
||||
}, description:
|
||||
'Do not supply this argument, it is automatically extracted ' +
|
||||
'from request headers.'
|
||||
}
|
||||
],
|
||||
http: {verb: 'all'}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
loopback.remoteMethod(
|
||||
UserModel.confirm,
|
||||
{
|
||||
|
@ -420,7 +476,7 @@ User.setup = function () {
|
|||
http: {verb: 'get', path: '/confirm'}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
loopback.remoteMethod(
|
||||
UserModel.resetPassword,
|
||||
{
|
||||
|
@ -440,16 +496,16 @@ User.setup = function () {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// default models
|
||||
UserModel.email = require('./email');
|
||||
UserModel.accessToken = require('./access-token');
|
||||
|
||||
|
||||
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
|
||||
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||
|
||||
|
||||
return UserModel;
|
||||
}
|
||||
|
||||
|
|
61
package.json
61
package.json
|
@ -9,40 +9,65 @@
|
|||
"Platform",
|
||||
"mBaaS"
|
||||
],
|
||||
"version": "1.5.3",
|
||||
"version": "1.7.7",
|
||||
"scripts": {
|
||||
"test": "mocha -R spec"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "~0.7.2",
|
||||
"express": "~3.4.0",
|
||||
"strong-remoting": "~1.1.0",
|
||||
"inflection": "~1.2.5",
|
||||
"passport": "~0.1.17",
|
||||
"debug": "~0.7.4",
|
||||
"express": "~3.4.8",
|
||||
"strong-remoting": "~1.3.1",
|
||||
"inflection": "~1.3.5",
|
||||
"passport": "~0.2.0",
|
||||
"passport-local": "~0.1.6",
|
||||
"nodemailer": "~0.5.7",
|
||||
"ejs": "~0.8.4",
|
||||
"bcryptjs": "~0.7.10",
|
||||
"nodemailer": "~0.6.0",
|
||||
"ejs": "~0.8.5",
|
||||
"bcryptjs": "~0.7.12",
|
||||
"underscore.string": "~2.3.3",
|
||||
"underscore": "~1.5.2",
|
||||
"underscore": "~1.6.0",
|
||||
"uid2": "0.0.3",
|
||||
"async": "~0.2.9",
|
||||
"async": "~0.2.10",
|
||||
"canonical-json": "0.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"loopback-datasource-juggler": "~1.2.11"
|
||||
"loopback-datasource-juggler": "~1.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"loopback-datasource-juggler": "~1.2.11",
|
||||
"mocha": "~1.14.0",
|
||||
"loopback-datasource-juggler": "~1.3.11",
|
||||
"mocha": "~1.17.1",
|
||||
"strong-task-emitter": "0.0.x",
|
||||
"supertest": "~0.8.1",
|
||||
"chai": "~1.8.1",
|
||||
"loopback-testing": "~0.1.0"
|
||||
"supertest": "~0.9.0",
|
||||
"chai": "~1.9.0",
|
||||
"loopback-testing": "~0.1.2",
|
||||
"browserify": "~3.41.0",
|
||||
"grunt": "~0.4.2",
|
||||
"grunt-browserify": "~1.3.1",
|
||||
"grunt-contrib-uglify": "~0.3.2",
|
||||
"grunt-contrib-jshint": "~0.8.0",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
"karma-chrome-launcher": "~0.1.2",
|
||||
"karma-firefox-launcher": "~0.1.3",
|
||||
"karma-html2js-preprocessor": "~0.1.0",
|
||||
"karma-phantomjs-launcher": "~0.1.2",
|
||||
"karma": "~0.10.9",
|
||||
"karma-browserify": "~0.2.0",
|
||||
"karma-mocha": "~0.1.1",
|
||||
"grunt-karma": "~0.6.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/strongloop/loopback"
|
||||
},
|
||||
"license": "MIT"
|
||||
"browser": {
|
||||
"express": "./lib/browser-express.js",
|
||||
"connect": false,
|
||||
"passport": false,
|
||||
"passport-local": false,
|
||||
"nodemailer": false
|
||||
},
|
||||
"license": {
|
||||
"name": "Dual MIT/StrongLoop",
|
||||
"url": "https://github.com/strongloop/loopback/blob/master/LICENSE"
|
||||
}
|
||||
}
|
||||
|
|
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);
|
||||
|
||||
/*
|
||||
describe('accessToken', function() {
|
||||
// it('should be a sublcass of AccessToken', function () {
|
||||
// assert(app.models.accessToken.prototype instanceof loopback.AccessToken);
|
||||
|
@ -54,6 +55,7 @@ describe('access control - integration', function () {
|
|||
return '/api/accessTokens/' + this.randomToken.id;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
describe('/users', function () {
|
||||
|
||||
|
@ -93,6 +95,10 @@ describe('access control - integration', function () {
|
|||
});
|
||||
lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
it('should not include a password', function() {
|
||||
var user = this.res.body;
|
||||
assert.equal(user.password, undefined);
|
||||
});
|
||||
});
|
||||
lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
|
@ -147,6 +153,7 @@ describe('access control - integration', function () {
|
|||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
|
||||
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts');
|
||||
|
@ -156,12 +163,25 @@ describe('access control - integration', function () {
|
|||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
|
||||
|
||||
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
||||
beforeEach(function() {
|
||||
this.url = '/api/accounts/' + this.user.accountId;
|
||||
beforeEach(function(done) {
|
||||
var self = this;
|
||||
|
||||
// Create an account under the given user
|
||||
app.models.account.create({
|
||||
userId: self.user.id,
|
||||
balance: 100
|
||||
}, function(err, act) {
|
||||
self.url = '/api/accounts/' + act.id;
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
|
||||
lt.it.shouldBeDenied();
|
||||
});
|
||||
|
|
|
@ -70,6 +70,40 @@ describe('security scopes', function () {
|
|||
});
|
||||
|
||||
describe('security ACLs', function () {
|
||||
it('should order ACL entries based on the matching score', function() {
|
||||
var acls = [
|
||||
{
|
||||
"model": "account",
|
||||
"accessType": "*",
|
||||
"permission": "DENY",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone"
|
||||
},
|
||||
{
|
||||
"model": "account",
|
||||
"accessType": "*",
|
||||
"permission": "ALLOW",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner"
|
||||
},
|
||||
{
|
||||
"model": "account",
|
||||
"accessType": "READ",
|
||||
"permission": "ALLOW",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone"
|
||||
}];
|
||||
var req = {
|
||||
model: 'account',
|
||||
property: 'find',
|
||||
accessType: 'WRITE'
|
||||
};
|
||||
var perm = ACL.resolvePermission(acls, req);
|
||||
assert.deepEqual(perm, { model: 'account',
|
||||
property: 'find',
|
||||
accessType: 'WRITE',
|
||||
permission: 'ALLOW' });
|
||||
});
|
||||
|
||||
it("should allow access to models for the given principal by wildcard", function () {
|
||||
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
|
||||
|
|
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.model(Model)', function() {
|
||||
var app, db;
|
||||
beforeEach(function() {
|
||||
app = loopback();
|
||||
db = loopback.createDataSource({connector: loopback.Memory});
|
||||
});
|
||||
|
||||
it("Expose a `Model` to remote clients", function() {
|
||||
var app = loopback();
|
||||
var memory = loopback.createDataSource({connector: loopback.Memory});
|
||||
var Color = memory.createModel('color', {name: String});
|
||||
var Color = db.createModel('color', {name: String});
|
||||
app.model(Color);
|
||||
assert.equal(app.models().length, 1);
|
||||
|
||||
expect(app.models()).to.eql([Color]);
|
||||
});
|
||||
|
||||
it('uses singlar name as app.remoteObjects() key', function() {
|
||||
var Color = db.createModel('color', {name: String});
|
||||
app.model(Color);
|
||||
expect(app.remoteObjects()).to.eql({ color: Color });
|
||||
});
|
||||
|
||||
it('uses singular name as shared class name', function() {
|
||||
var Color = db.createModel('color', {name: String});
|
||||
app.model(Color);
|
||||
expect(app.remotes().exports).to.eql({ color: Color });
|
||||
});
|
||||
|
||||
it('updates REST API when a new model is added', function(done) {
|
||||
app.use(loopback.rest());
|
||||
request(app).get('/colors').expect(404, function(err, res) {
|
||||
if (err) return done(err);
|
||||
var Color = db.createModel('color', {name: String});
|
||||
app.model(Color);
|
||||
request(app).get('/colors').expect(200, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in compat mode', function() {
|
||||
before(function() {
|
||||
loopback.compat.usePluralNamesForRemoting = true;
|
||||
});
|
||||
after(function() {
|
||||
loopback.compat.usePluralNamesForRemoting = false;
|
||||
});
|
||||
|
||||
it('uses plural name as shared class name', function() {
|
||||
var Color = db.createModel('color', {name: String});
|
||||
app.model(Color);
|
||||
expect(app.remotes().exports).to.eql({ colors: Color });
|
||||
});
|
||||
|
||||
it('uses plural name as app.remoteObjects() key', function() {
|
||||
var Color = db.createModel('color', {name: String});
|
||||
app.model(Color);
|
||||
expect(app.remoteObjects()).to.eql({ colors: Color });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -36,13 +84,20 @@ describe('app', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('app.models', function() {
|
||||
it('is unique per app instance', function() {
|
||||
var Color = app.model('Color', { dataSource: 'db' });
|
||||
expect(app.models.Color).to.equal(Color);
|
||||
var anotherApp = loopback();
|
||||
expect(anotherApp.models.Color).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('app.boot([options])', function () {
|
||||
beforeEach(function () {
|
||||
var app = this.app = loopback();
|
||||
|
||||
app.boot({
|
||||
app: {
|
||||
port: 3000,
|
||||
port: 3000,
|
||||
host: '127.0.0.1',
|
||||
restApiRoot: '/rest-api',
|
||||
foo: {bar: 'bat'},
|
||||
|
@ -59,7 +114,7 @@ describe('app', function() {
|
|||
dataSources: {
|
||||
'the-db': {
|
||||
connector: 'memory'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -106,14 +161,14 @@ describe('app', function() {
|
|||
var app = loopback();
|
||||
app.boot({
|
||||
app: {
|
||||
port: undefined,
|
||||
port: undefined,
|
||||
host: undefined
|
||||
}
|
||||
});
|
||||
return app;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should be honored', function() {
|
||||
var assertHonored = function (portKey, hostKey) {
|
||||
process.env[hostKey] = randomPort();
|
||||
|
@ -142,6 +197,12 @@ describe('app', function() {
|
|||
var app = this.boot();
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
|
||||
delete process.env.npm_config_host;
|
||||
delete process.env.OPENSHIFT_SLS_IP;
|
||||
delete process.env.OPENSHIFT_NODEJS_IP;
|
||||
delete process.env.HOST;
|
||||
delete process.env.npm_package_config_host;
|
||||
|
||||
process.env.npm_config_port = randomPort();
|
||||
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
||||
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
||||
|
@ -151,6 +212,12 @@ describe('app', function() {
|
|||
var app = this.boot();
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
assert.equal(app.get('port'), process.env.npm_config_port);
|
||||
|
||||
delete process.env.npm_config_port;
|
||||
delete process.env.OPENSHIFT_SLS_PORT;
|
||||
delete process.env.OPENSHIFT_NODEJS_PORT;
|
||||
delete process.env.PORT;
|
||||
delete process.env.npm_package_config_port;
|
||||
});
|
||||
|
||||
function randomHost() {
|
||||
|
@ -160,6 +227,18 @@ describe('app', function() {
|
|||
function randomPort() {
|
||||
return Math.floor(Math.random() * 10000);
|
||||
}
|
||||
|
||||
it('should honor 0 for free port', function () {
|
||||
var app = loopback();
|
||||
app.boot({app: {port: 0}});
|
||||
assert.equal(app.get('port'), 0);
|
||||
});
|
||||
|
||||
it('should default to port 3000', function () {
|
||||
var app = loopback();
|
||||
app.boot({app: {port: undefined}});
|
||||
assert.equal(app.get('port'), 3000);
|
||||
});
|
||||
});
|
||||
|
||||
it('Instantiate models', function () {
|
||||
|
@ -353,6 +432,14 @@ describe('app', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('enableAuth', function() {
|
||||
it('should set app.isAuthEnabled to true', function() {
|
||||
expect(app.isAuthEnabled).to.not.equal(true);
|
||||
app.enableAuth();
|
||||
expect(app.isAuthEnabled).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('app.get("/", loopback.status())', function () {
|
||||
it('should return the status of the application', function (done) {
|
||||
var app = loopback();
|
||||
|
@ -365,7 +452,8 @@ describe('app', function() {
|
|||
|
||||
assert.equal(typeof res.body, 'object');
|
||||
assert(res.body.started);
|
||||
assert(res.body.uptime);
|
||||
// The number can be 0
|
||||
assert(res.body.uptime !== undefined);
|
||||
|
||||
var elapsed = Date.now() - Number(new Date(res.body.started));
|
||||
|
||||
|
|
|
@ -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",
|
||||
"foreignKey": "userId"
|
||||
},
|
||||
"account": {
|
||||
"model": "account",
|
||||
"type": "belongsTo"
|
||||
},
|
||||
"transactions": {
|
||||
"model": "transaction",
|
||||
"type": "hasMany"
|
||||
|
@ -103,6 +99,11 @@
|
|||
"transactions": {
|
||||
"model": "transaction",
|
||||
"type": "hasMany"
|
||||
},
|
||||
"user": {
|
||||
"model": "user",
|
||||
"type": "belongsTo",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
|
|
|
@ -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')}));
|
||||
var apiPath = '/api';
|
||||
app.use(apiPath, loopback.rest());
|
||||
app.use(app.router);
|
||||
app.use(loopback.static(path.join(__dirname, 'public')));
|
||||
app.use(loopback.urlNotFound());
|
||||
app.use(loopback.errorHandler());
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
{
|
||||
"port": 3000,
|
||||
"host": "0.0.0.0",
|
||||
"cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0"
|
||||
"cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0",
|
||||
"remoting": {
|
||||
"json": {
|
||||
"limit": "1kb",
|
||||
"strict": false
|
||||
},
|
||||
"urlencoded": {
|
||||
"limit": "8kb"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,15 @@
|
|||
"widget": {
|
||||
"properties": {},
|
||||
"public": true,
|
||||
"dataSource": "db"
|
||||
"dataSource": "db",
|
||||
"options": {
|
||||
"relations": {
|
||||
"store": {
|
||||
"model": "store",
|
||||
"type": "belongsTo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"store": {
|
||||
"properties": {},
|
||||
|
|
|
@ -53,4 +53,4 @@ describe('GeoPoint', function() {
|
|||
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.created);
|
||||
assert(app.modified);
|
||||
assert.equal(typeof app.id, 'string');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
@ -121,7 +122,8 @@ describe('Application', function () {
|
|||
it('Authenticate with application id & clientKey', function (done) {
|
||||
Application.authenticate(registeredApp.id, registeredApp.clientKey,
|
||||
function (err, result) {
|
||||
assert.equal(result, 'clientKey');
|
||||
assert.equal(result.application.id, registeredApp.id);
|
||||
assert.equal(result.keyType, 'clientKey');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
@ -129,7 +131,8 @@ describe('Application', function () {
|
|||
it('Authenticate with application id & javaScriptKey', function (done) {
|
||||
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey,
|
||||
function (err, result) {
|
||||
assert.equal(result, 'javaScriptKey');
|
||||
assert.equal(result.application.id, registeredApp.id);
|
||||
assert.equal(result.keyType, 'javaScriptKey');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
@ -137,7 +140,8 @@ describe('Application', function () {
|
|||
it('Authenticate with application id & restApiKey', function (done) {
|
||||
Application.authenticate(registeredApp.id, registeredApp.restApiKey,
|
||||
function (err, result) {
|
||||
assert.equal(result, 'restApiKey');
|
||||
assert.equal(result.application.id, registeredApp.id);
|
||||
assert.equal(result.keyType, 'restApiKey');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
@ -145,7 +149,8 @@ describe('Application', function () {
|
|||
it('Authenticate with application id & masterKey', function (done) {
|
||||
Application.authenticate(registeredApp.id, registeredApp.masterKey,
|
||||
function (err, result) {
|
||||
assert.equal(result, 'masterKey');
|
||||
assert.equal(result.application.id, registeredApp.id);
|
||||
assert.equal(result.keyType, 'masterKey');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
@ -153,7 +158,8 @@ describe('Application', function () {
|
|||
it('Authenticate with application id & windowsKey', function (done) {
|
||||
Application.authenticate(registeredApp.id, registeredApp.windowsKey,
|
||||
function (err, result) {
|
||||
assert.equal(result, 'windowsKey');
|
||||
assert.equal(result.application.id, registeredApp.id);
|
||||
assert.equal(result.keyType, 'windowsKey');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
@ -170,13 +176,14 @@ describe('Application', function () {
|
|||
describe('Application subclass', function () {
|
||||
it('should use subclass model name', function (done) {
|
||||
var MyApp = Application.extend('MyApp');
|
||||
MyApp.attachTo(loopback.createDataSource({connector: loopback.Memory}));
|
||||
MyApp.register('rfeng', 'MyApp2',
|
||||
{description: 'My second mobile application'}, function (err, result) {
|
||||
var ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
MyApp.attachTo(ds);
|
||||
MyApp.register('rfeng', 'MyApp123',
|
||||
{description: 'My 123 mobile application'}, function (err, result) {
|
||||
var app = result;
|
||||
assert.equal(app.owner, 'rfeng');
|
||||
assert.equal(app.name, 'MyApp2');
|
||||
assert.equal(app.description, 'My second mobile application');
|
||||
assert.equal(app.name, 'MyApp123');
|
||||
assert.equal(app.description, 'My 123 mobile application');
|
||||
assert(app.clientKey);
|
||||
assert(app.javaScriptKey);
|
||||
assert(app.restApiKey);
|
||||
|
@ -184,14 +191,17 @@ describe('Application subclass', function () {
|
|||
assert(app.masterKey);
|
||||
assert(app.created);
|
||||
assert(app.modified);
|
||||
MyApp.findById(app.id, function (err, myApp) {
|
||||
assert(!err);
|
||||
assert(myApp);
|
||||
|
||||
Application.findById(app.id, function (err, myApp) {
|
||||
// Remove all instances from Application model to avoid left-over data
|
||||
Application.destroyAll(function () {
|
||||
MyApp.findById(app.id, function (err, myApp) {
|
||||
assert(!err);
|
||||
assert(myApp === null);
|
||||
done(err, myApp);
|
||||
assert(myApp);
|
||||
|
||||
Application.findById(app.id, function (err, myApp) {
|
||||
assert(!err);
|
||||
assert(myApp === null);
|
||||
done(err, myApp);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var async = require('async');
|
||||
require('./support');
|
||||
var loopback = require('../');
|
||||
var ACL = loopback.ACL;
|
||||
var Change = loopback.Change;
|
||||
|
@ -197,21 +198,21 @@ describe('Model', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Model.deleteById([callback])', function () {
|
||||
it("Delete a model instance from the attached data source", function (done) {
|
||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||
User.deleteById(user.id, function (err) {
|
||||
User.findById(user.id, function (err, notFound) {
|
||||
assert(!err);
|
||||
assert.equal(notFound, null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Model.deleteById([callback])', function () {
|
||||
it("Delete a model instance from the attached data source", function (done) {
|
||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||
User.deleteById(user.id, function (err) {
|
||||
User.findById(user.id, function (err, notFound) {
|
||||
assert(!err);
|
||||
assert.equal(notFound, null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model.destroyAll(callback)', function() {
|
||||
describe('Model.destroyAll(callback)', function() {
|
||||
it("Delete all Model instances from data source", function(done) {
|
||||
(new TaskEmitter())
|
||||
.task(User, 'create', {first: 'jill'})
|
||||
|
@ -263,7 +264,8 @@ describe('Model', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Remote Methods', function(){
|
||||
describe.onServer('Remote Methods', function(){
|
||||
|
||||
beforeEach(function () {
|
||||
User.login = function (username, password, fn) {
|
||||
if(username === 'foo' && password === 'bar') {
|
||||
|
@ -429,6 +431,37 @@ describe('Model', function() {
|
|||
});
|
||||
});
|
||||
})
|
||||
|
||||
describe('in compat mode', function() {
|
||||
before(function() {
|
||||
loopback.compat.usePluralNamesForRemoting = true;
|
||||
});
|
||||
after(function() {
|
||||
loopback.compat.usePluralNamesForRemoting = false;
|
||||
});
|
||||
|
||||
it('correctly install before/after hooks', function(done) {
|
||||
var hooksCalled = [];
|
||||
|
||||
User.beforeRemote('**', function(ctx, user, next) {
|
||||
hooksCalled.push('beforeRemote');
|
||||
next();
|
||||
});
|
||||
|
||||
User.afterRemote('**', function(ctx, user, next) {
|
||||
hooksCalled.push('afterRemote');
|
||||
next();
|
||||
});
|
||||
|
||||
request(app).get('/users')
|
||||
.expect(200, function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(hooksCalled, 'hooks called')
|
||||
.to.eql(['beforeRemote', 'afterRemote']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model.hasMany(Model)', function() {
|
||||
|
@ -705,4 +738,14 @@ describe('Model', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model._getACLModel()', function() {
|
||||
it('should return the subclass of ACL', function() {
|
||||
var Model = require('../').Model;
|
||||
var acl = ACL.extend('acl');
|
||||
Model._ACL(null); // Reset the ACL class for the base model
|
||||
var model = Model._ACL();
|
||||
assert.equal(model, acl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ var path = require('path');
|
|||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app');
|
||||
var app = require(path.join(SIMPLE_APP, 'app.js'));
|
||||
var assert = require('assert');
|
||||
var expect = require('chai').expect;
|
||||
|
||||
describe('relations - integration', function () {
|
||||
|
||||
|
@ -95,4 +96,132 @@ describe('relations - integration', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('/widgets/:id/store', function () {
|
||||
beforeEach(function (done) {
|
||||
var self = this;
|
||||
this.store.widgets.create({
|
||||
name: this.widgetName
|
||||
}, function(err, widget) {
|
||||
self.widget = widget;
|
||||
self.url = '/api/widgets/' + self.widget.id + '/store';
|
||||
done();
|
||||
});
|
||||
});
|
||||
lt.describe.whenCalledRemotely('GET', '/api/widgets/:id/store', function () {
|
||||
it('should succeed with statusCode 200', function () {
|
||||
assert.equal(this.res.statusCode, 200);
|
||||
assert.equal(this.res.body.id, this.store.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasAndBelongsToMany', function() {
|
||||
beforeEach(function defineProductAndCategoryModels() {
|
||||
var product = app.model(
|
||||
'product',
|
||||
{ properties: { id: 'string', name: 'string' }, dataSource: 'db' }
|
||||
|
||||
);
|
||||
var category = app.model(
|
||||
'category',
|
||||
{ properties: { id: 'string', name: 'string' }, dataSource: 'db' }
|
||||
);
|
||||
product.hasAndBelongsToMany(category);
|
||||
category.hasAndBelongsToMany(product);
|
||||
});
|
||||
|
||||
lt.beforeEach.givenModel('category');
|
||||
|
||||
beforeEach(function createProductsInCategory(done) {
|
||||
var test = this;
|
||||
this.category.products.create({
|
||||
name: 'a-product'
|
||||
}, function(err, product) {
|
||||
if (err) return done(err);
|
||||
test.product = product;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function createAnotherCategoryAndProduct(done) {
|
||||
app.models.category.create({ name: 'another-category' },
|
||||
function(err, cat) {
|
||||
if (err) return done(err);
|
||||
cat.products.create({ name: 'another-product' }, done);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
this.app.models.product.destroyAll(done);
|
||||
});
|
||||
|
||||
it.skip('allows to find related objects via where filter', function(done) {
|
||||
//TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94
|
||||
var expectedProduct = this.product;
|
||||
// Note: the URL format is not final
|
||||
this.get('/api/products?filter[where][categoryId]=' + this.category.id)
|
||||
.expect(200, function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(res.body).to.eql([
|
||||
{
|
||||
id: expectedProduct.id,
|
||||
name: expectedProduct.name
|
||||
}
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows to find related object via URL scope', function(done) {
|
||||
var expectedProduct = this.product;
|
||||
this.get('/api/categories/' + this.category.id + '/products')
|
||||
.expect(200, function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(res.body).to.eql([
|
||||
{
|
||||
id: expectedProduct.id,
|
||||
name: expectedProduct.name
|
||||
}
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('includes requested related models in `find`', function(done) {
|
||||
var expectedProduct = this.product;
|
||||
var url = '/api/categories/findOne?filter[where][id]=' +
|
||||
this.category.id + '&filter[include]=products';
|
||||
|
||||
this.get(url)
|
||||
.expect(200, function(err, res) {
|
||||
expect(res.body).to.have.property('products');
|
||||
expect(res.body.products).to.eql([
|
||||
{
|
||||
id: expectedProduct.id,
|
||||
name: expectedProduct.name
|
||||
}
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('includes requested related models in `findById`', function(done) {
|
||||
//TODO https://github.com/strongloop/loopback-datasource-juggler/issues/93
|
||||
var expectedProduct = this.product;
|
||||
// Note: the URL format is not final
|
||||
var url = '/api/categories/' + this.category.id + '?include=products';
|
||||
|
||||
this.get(url)
|
||||
.expect(200, function(err, res) {
|
||||
expect(res.body).to.have.property('products');
|
||||
expect(res.body.products).to.eql([
|
||||
{
|
||||
id: expectedProduct.id,
|
||||
name: expectedProduct.name
|
||||
}
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
ds = loopback.createDataSource({connector: 'memory'});
|
||||
// Re-attach the models so that they can have isolated store to avoid
|
||||
// pollutions from other tests
|
||||
User.attachTo(ds);
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
|
|
|
@ -11,13 +11,12 @@ app = null;
|
|||
TaskEmitter = require('strong-task-emitter');
|
||||
request = require('supertest');
|
||||
|
||||
|
||||
// Speed up the password hashing algorithm
|
||||
// for tests using the built-in User model
|
||||
loopback.User.settings.saltWorkFactor = 4;
|
||||
|
||||
beforeEach(function () {
|
||||
app = loopback();
|
||||
this.app = app = loopback();
|
||||
|
||||
// setup default data sources
|
||||
loopback.setDefaultDataSourceForType('db', {
|
||||
|
@ -50,3 +49,19 @@ assert.isFunc = function (obj, name) {
|
|||
assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist');
|
||||
assert(typeof obj[name] === 'function', name + ' is not a function');
|
||||
}
|
||||
|
||||
describe.onServer = function describeOnServer(name, fn) {
|
||||
if (loopback.isServer) {
|
||||
describe(name, fn);
|
||||
} else {
|
||||
describe.skip(name, fn);
|
||||
}
|
||||
};
|
||||
|
||||
describe.inBrowser = function describeInBrowser(name, fn) {
|
||||
if (loopback.isBrowser) {
|
||||
describe(name, fn);
|
||||
} else {
|
||||
describe.skip(name, fn);
|
||||
}
|
||||
};
|
|
@ -4,20 +4,25 @@ var passport = require('passport');
|
|||
var MailConnector = require('../lib/connectors/mail');
|
||||
|
||||
var userMemory = loopback.createDataSource({
|
||||
connector: loopback.Memory
|
||||
connector: 'memory'
|
||||
});
|
||||
|
||||
describe('User', function(){
|
||||
var validCredentials = {email: 'foo@bar.com', password: 'bar'};
|
||||
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
|
||||
var incompleteCredentials = {password: 'bar1'};
|
||||
|
||||
beforeEach(function() {
|
||||
User = loopback.User.extend('user');
|
||||
User.email = loopback.Email.extend('email');
|
||||
loopback.autoAttach();
|
||||
|
||||
// Update the AccessToken relation to use the subclass of User
|
||||
AccessToken.belongsTo(User);
|
||||
|
||||
// allow many User.afterRemote's to be called
|
||||
User.setMaxListeners(0);
|
||||
|
||||
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
|
||||
AccessToken.belongsTo(User);
|
||||
|
||||
});
|
||||
|
||||
beforeEach(function (done) {
|
||||
|
@ -25,7 +30,7 @@ describe('User', function(){
|
|||
app.use(loopback.rest());
|
||||
app.model(User);
|
||||
|
||||
User.create({email: 'foo@bar.com', password: 'bar'}, done);
|
||||
User.create(validCredentials, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
|
@ -105,7 +110,7 @@ describe('User', function(){
|
|||
|
||||
describe('User.login', function() {
|
||||
it('Login a user by providing credentials', function(done) {
|
||||
User.login({email: 'foo@bar.com', password: 'bar'}, function (err, accessToken) {
|
||||
User.login(validCredentials, function (err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
|
@ -119,7 +124,7 @@ describe('User', function(){
|
|||
.post('/users/login')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.send({email: 'foo@bar.com', password: 'bar'})
|
||||
.send(validCredentials)
|
||||
.end(function(err, res){
|
||||
if(err) return done(err);
|
||||
var accessToken = res.body;
|
||||
|
@ -127,11 +132,62 @@ describe('User', function(){
|
|||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
assert(accessToken.user === undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Login a user over REST by providing invalid credentials', function(done) {
|
||||
request(app)
|
||||
.post('/users/login')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(401)
|
||||
.send(invalidCredentials)
|
||||
.end(function(err, res){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Login a user over REST by providing incomplete credentials', function(done) {
|
||||
request(app)
|
||||
.post('/users/login')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(400)
|
||||
.send(incompleteCredentials)
|
||||
.end(function(err, res){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Login a user over REST with the wrong Content-Type', function(done) {
|
||||
request(app)
|
||||
.post('/users/login')
|
||||
.set('Content-Type', null)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(400)
|
||||
.send(validCredentials)
|
||||
.end(function(err, res){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns current user when `include` is `USER`', function(done) {
|
||||
request(app)
|
||||
.post('/users/login?include=USER')
|
||||
.send(validCredentials)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
var token = res.body;
|
||||
expect(token.user, 'body.user').to.not.equal(undefined);
|
||||
expect(token.user, 'body.user')
|
||||
.to.have.property('email', validCredentials.email);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Login should only allow correct credentials', function(done) {
|
||||
User.create({email: 'foo22@bar.com', password: 'bar'}, function(user, err) {
|
||||
User.login({email: 'foo44@bar.com', password: 'bar'}, function(err, accessToken) {
|
||||
|
|
Loading…
Reference in New Issue