diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..4ec275ddd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,39 @@
+
+
+### Bug or feature request
+
+
+
+- [ ] Bug
+- [ ] Feature request
+
+### Description of feature (or steps to reproduce if bug)
+
+
+
+### Link to sample repo to reproduce issue (if bug)
+
+
+
+### Expected result
+
+
+
+### Actual result (if bug)
+
+
+
+### Additional information (Node.js version, LoopBack version, etc)
+
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..58c8d7b2d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,19 @@
+### Description
+
+
+#### Related issues
+
+
+
+- None
+
+### Checklist
+
+- [ ] New tests are added to cover all changes
+- [ ] Code conforms with the [style
+ guide](http://loopback.io/doc/en/contrib/style-guide.html)
diff --git a/.gitignore b/.gitignore
index a30033bdf..ba976d833 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
node_modules
dist
*xunit.xml
+.nyc_output/
diff --git a/.jscsrc b/.jscsrc
index 8deeea1cf..b2569dc0c 100644
--- a/.jscsrc
+++ b/.jscsrc
@@ -10,14 +10,11 @@
],
"disallowMultipleVarDecl": "exceptUndefined",
"disallowSpacesInsideObjectBrackets": null,
+ "jsDoc": false,
+ "requireDotNotation": false,
"maximumLineLength": {
"value": 150,
"allowComments": true,
"allowRegex": true
- },
- "validateJSDoc": {
- "checkParamNames": false,
- "checkRedundantParams": false,
- "requireParamTypes": true
}
}
diff --git a/.nycrc b/.nycrc
new file mode 100644
index 000000000..b64607eee
--- /dev/null
+++ b/.nycrc
@@ -0,0 +1,7 @@
+{
+ "exclude": [
+ "Gruntfile.js",
+ "test/**/*.js"
+ ],
+ "cache": true
+}
diff --git a/.strong-pm/env.json b/.strong-pm/env.json
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.travis.yml b/.travis.yml
index 168369451..bdf39070f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,21 @@
sudo: false
language: node_js
node_js:
- - "0.10"
- - "0.12"
- - "iojs"
+ - "4"
+ - "6"
+ - "8"
+after_success: npm run coverage
+
+# see https://www.npmjs.com/package/phantomjs-prebuilt#continuous-integration
+cache:
+ directories:
+ - travis_phantomjs
+before_install:
+ # Upgrade PhantomJS to v2.1.1.
+ - "export PHANTOMJS_VERSION=2.1.1"
+ - "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH"
+ - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi"
+ - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi"
+ - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi"
+ - "phantomjs --version"
diff --git a/CHANGES.md b/CHANGES.md
index ea87f5de4..54c931e59 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,432 @@
+2019-06-04, Version 2.42.0
+==========================
+
+ * fix: disallow queries in username and email fields (Hage Yaapa)
+
+ * update README for 2.x EOL (Diana Lau)
+
+
+2019-01-11, Version 2.41.2
+==========================
+
+ * Fix crash when modifying an unknown user (Matheus Horstmann)
+
+ * test: fix User test for custom token generator (Miroslav Bajtoš)
+
+ * test: improve assertion error messages (Miroslav Bajtoš)
+
+
+2018-11-26, Version 2.41.1
+==========================
+
+ * Fix: treat empty access token string as undefined (andrey-abramow)
+
+ * Fix context propagation broken by async@2.x (Miroslav Bajtoš)
+
+
+2018-10-09, Version 2.41.0
+==========================
+
+ * Update LB2 LTS version (Diana Lau)
+
+
+2018-08-08, Version 2.40.0
+==========================
+
+ * fix: accessToken create default acl (virkt25)
+
+
+2018-02-12, Version 2.39.2
+==========================
+
+ * Babelify juggler for Karma tests (Miroslav Bajtoš)
+
+ * Fix Karma config to babelify node_modules too (Miroslav Bajtoš)
+
+
+2018-01-31, Version 2.39.1
+==========================
+
+ * update juggler dep (Taranveer Virk)
+
+ * fix(id): replace with != null (Samuel Reed)
+
+ * fix(AccessContext): Tighten userid/appid checks (Samuel Reed)
+
+
+2017-10-23, Version 2.39.0
+==========================
+
+ * Drop support for Node.js versions 0.10 and 0.12 (Miroslav Bajtoš)
+
+ * test: fix too strict test assertion (Miroslav Bajtoš)
+
+ * Add unit test for empty password (loay)
+
+ * Update translated strings Q2 2017 (Allen Boone)
+
+
+2017-04-17, Version 2.38.3
+==========================
+
+ * use lower version of karma-browserify (Diana Lau)
+
+ * update karma-browserify to 5.x (Diana Lau)
+
+ * update translation msg (Diana Lau)
+
+ * Fix user-literal rewrite for anonymous requests (Aaron Buchanan)
+
+ * Forward options in prepareForTokenInvalidation (Miroslav Bajtoš)
+
+
+2017-03-17, Version 2.38.2
+==========================
+
+ * Fix file patch (Raymond Feng)
+
+ * Add nyc coverage, report data to coveralls.io (Miroslav Bajtoš)
+
+
+2017-03-13, Version 2.38.1
+==========================
+
+ * Fix User.verify to convert uid to string (phairow)
+
+ * Configure Travis CI to cache phantomjs binaries (Miroslav Bajtoš)
+
+ * Improve "filter" arg description (Raymond Camden)
+
+ * Fix creation of verification links (Miroslav Bajtoš)
+
+ * Include link to docs in logoutSessions warning (Miroslav Bajtoš)
+
+ * Fix detection of logoutSessionsOnSensitiveChanges (Miroslav Bajtoš)
+
+ * Preserve sessions on User.save() making no changes (Miroslav Bajtoš)
+
+ * Remove unused dependencies (Miroslav Bajtoš)
+
+ * Fix logout to handle no or missing accessToken (Ritchie Martori)
+
+ * Use English when running Mocha tests (Miroslav Bajtoš)
+
+ * Role model: resolves related models by name (Benjamin Kroeger)
+
+ * Fix User methods to use correct Primary Key (Aris Kemper)
+
+
+2017-01-20, Version 2.38.0
+==========================
+
+ * Add app setting logoutSessionsOnSensitiveChanges (Miroslav Bajtoš)
+
+ * Fix User.resetPassword to call createAccessToken() (João Ribeiro)
+
+
+2017-01-16, Version 2.37.1
+==========================
+
+ * Preserve current session when invalidating tokens (Miroslav Bajtoš)
+
+ * Clean up access-token-invalidation tests (Miroslav Bajtoš)
+
+
+2017-01-09, Version 2.37.0
+==========================
+
+ * Emit resetPasswordRequest event with options (Sergey Reus)
+
+ * Fix false emailVerified on user model update (박대선)
+
+ * Add new flag injectOptionsFromRemoteContext (Miroslav Bajtoš)
+
+ * Contextify DAO and relation methods (Miroslav Bajtoš)
+
+ * Implement new http arg mapping optionsFromRequest (Miroslav Bajtoš)
+
+ * Fix package.json CI downstreamIgnoreList nesting (David Cheung)
+
+
+2016-12-21, Version 2.36.2
+==========================
+
+ * Add option disabling periodic change rectification (kobaska)
+
+ * Release LTS LB2 (Simon Ho)
+
+ * Invalidate AccessTokens on password change (Miroslav Bajtoš)
+
+ * Fix registration of operation hooks in User model (Miroslav Bajtoš)
+
+ * Remove "options.template" from Email payload (Miroslav Bajtoš)
+
+ * Opt-out downstream builds that are unstable (David Cheung)
+
+ * Allow password reset request for users in realms (Bram Borggreve)
+
+ * Add "returnOnlyRoleNames" option to Role.getRoles (Eric)
+
+ * Fix context within listByPrincipalType role method (codyolsen)
+
+ * Add templateFn option to User#verify() (Adrien Kiren)
+
+ * Add options to bulkUpdate (Kogulan Baskaran)
+
+ * Require verification after email change (Loay)
+
+ * adding check of string for case insensitive emails (Dhaval Trivedi)
+
+ * Fix PR template to not link all PRs to #49 (#2887) (Miroslav Bajtoš)
+
+
+2016-10-24, Version 2.36.0
+==========================
+
+ * Need index on principalId for performance. (#2883) (#2884) (Simon Ho)
+
+ * Remove redundant items in PR template (#2877) (#2878) (Simon Ho)
+
+ * Refactor PR template based on feedback (#2865) (#2874) (Simon Ho)
+
+ * Add pull request template (#2843) (#2862) (Simon Ho)
+
+ * Fix description of updateAll response (Miroslav Bajtoš)
+
+
+2016-10-13, Version 2.35.0
+==========================
+
+ * Reword ticking checkbox note in issue template (#2855) (Simon Ho)
+
+ * Add how to tick checkbox in issue template (#2851) (#2853) (Simon Ho)
+
+ * Use GitHub issue templates (#2810) (#2852) (Simon Ho)
+
+ * Allow tokens with eternal TTL (value -1) (Miroslav Bajtoš)
+
+ * Update ja and nl translation files (Candy)
+
+ * Fix support for remote hooks returning a Promise (Tim van der Staaij)
+
+ * Validate non-email property partial update (Loay)
+
+ * Update translation files - round#2 (Candy)
+
+ * Update tests to use registry for model creation (gunjpan)
+
+ * Call new disable remote method from model class. (Richard Pringle)
+
+ * Temporarily disable Karma tests on Windows CI (Miroslav Bajtoš)
+
+ * Add translation files for 2.x (Candy)
+
+ * Allow resetPassword if email is verified (Loay)
+
+ * Add docs for KeyValue model (Simon Ho)
+
+ * Invalidate sessions after email change (Loay)
+
+ * Upgrade loopback-testing to the latest ^1.4 (Miroslav Bajtoš)
+
+
+2016-09-13, Version 2.34.1
+==========================
+
+ * Fix double-slash in confirmation URL (Miroslav Bajtoš)
+
+
+2016-09-12, Version 2.34.0
+==========================
+
+
+
+2016-09-09, Version 2.33.0
+==========================
+
+ * Fix data argument for upsertWithWhere (Amir Jafarian)
+
+ * Expose upsertWithWhere (Sonali Samantaray)
+
+ * Fix remoting metadata for "data" arguments (Miroslav Bajtoš)
+
+ * Rework email validation to use isemail (Miroslav Bajtoš)
+
+
+2016-09-05, Version 2.32.0
+==========================
+
+ * test/user: don't attach User model twice (Miroslav Bajtoš)
+
+ * app.enableAuth: correctly detect attached models (Miroslav Bajtoš)
+
+ * Apply g.f to literal strings (Candy)
+
+ * Make the app instance available to connectors (Subramanian Krishnan)
+
+ * Reorder PATCH Vs PUT endpoints (Amir Jafarian)
+
+ * streamline use if `self` (Benjamin Kroeger)
+
+ * resolve related models from correct registry (Benjamin Kroeger)
+
+ * KeyValueModel: add API for listing keys (Miroslav Bajtoš)
+
+
+2016-08-17, Version 2.31.0
+==========================
+
+ * Fix token middleware crash (Carl Fürstenberg)
+
+ * Support 'alias' in mail transport config. (Samuel Reed)
+
+
+2016-08-16, Version 2.30.0
+==========================
+
+ * Revert globalization of Swagger descriptions (Miroslav Bajtoš)
+
+ * Expose `Replace*` methods (Amir Jafarian)
+
+ * Add bcrypt validation (Loay)
+
+ * Cache remoting descriptions to speed up tests (Miroslav Bajtoš)
+
+ * Revert globalization of assert() messages (Miroslav Bajtoš)
+
+ * Fix token middleware to not trigger CLS init (Miroslav Bajtoš)
+
+ * common: add KeyValueModel (Miroslav Bajtoš)
+
+ * Globalize current-context deprecation messages (Miroslav Bajtoš)
+
+ * Deprecate current-context API (Miroslav Bajtoš)
+
+ * test: increase timeout to prevent CI failures (Miroslav Bajtoš)
+
+ * Backport of #2407 (Candy)
+
+ * test: fix timeout in rest.middleware.test (Miroslav Bajtoš)
+
+ * test: fix "socket hang up" error in app.test (Miroslav Bajtoš)
+
+ * test: increate timeout in Role test (Miroslav Bajtoš)
+
+ * test: make status test more robust (Miroslav Bajtoš)
+
+ * test: fix broken Role tests (Miroslav Bajtoš)
+
+ * Update dependencies to their latest versions (Miroslav Bajtoš)
+
+ * Increase timeout (jannyHou)
+
+ * Backport of #2565 (Miroslav Bajtoš)
+
+ * Avoid calling deprecated methds (Amir Jafarian)
+
+ * test: use local registry in test fixtures (Miroslav Bajtoš)
+
+ * Fix test case error (Loay)
+
+ * Backport/Fix security issue 580 (Loay)
+
+
+2016-07-12, Version 2.29.1
+==========================
+
+ * Fix description for User.prototype.hasPassword (Jue Hou)
+
+ * Fix verificationToken bug #2440 (Loay)
+
+ * add missing unit tests for #2108 (Benjamin Kroeger)
+
+
+2016-06-07, Version 2.29.0
+==========================
+
+ * test: increase timeouts on CI (Miroslav Bajtoš)
+
+ * jscsrc: remove jsDoc rule (Miroslav Bajtoš)
+
+ * Deprecate getters for express 3.x middleware (Miroslav Bajtoš)
+
+ * Remove env.json and strong-pm dir (Ritchie Martori)
+
+ * Fix JSCS unsupported rule error (Jason)
+
+ * Resolver support return promise (juehou)
+
+ * Update user.js (Rik)
+
+ * Backport separate error checking and done logic (Simon Ho)
+
+ * Clean up by removing unnecessary comments (Supasate Choochaisri)
+
+ * Add feature to not allow duplicate role name (Supasate Choochaisri)
+
+ * update/insert copyright notices (Ryan Graham)
+
+ * relicense as MIT only (Ryan Graham)
+
+ * Upgrade phantomjs to 2.x (Miroslav Bajtoš)
+
+ * app: send port:0 instead of port:undefined (Miroslav Bajtoš)
+
+ * travis: drop node@5, add node@6 (Miroslav Bajtoš)
+
+ * Disable DEBUG output for eslint on Jenkins CI (Miroslav Bajtoš)
+
+ * test/rest.middleware: use local registry (Miroslav Bajtoš)
+
+ * Fix role.isOwner to support app-local registry (Miroslav Bajtoš)
+
+ * test/user: use local registry (Miroslav Bajtoš)
+
+
+2016-05-02, Version 2.28.0
+==========================
+
+ * Add new feature to emit a `remoteMethodDisabled` event when disabling a remote method. (Supasate Choochaisri)
+
+ * Fix typo in Model.nestRemoting (Tim Needham)
+
+ * Allow built-in token middleware to run repeatedly (Benjamin Kröger)
+
+ * Improve error message on connector init error (Miroslav Bajtoš)
+
+ * application: correct spelling of "cannont" (Sam Roberts)
+
+
+2016-02-19, Version 2.27.0
+==========================
+
+ * Remove sl-blip from dependency (Candy)
+
+ * Fix race condition in replication tests (Miroslav Bajtoš)
+
+ * test: remove errant console.log from test (Ryan Graham)
+
+ * Promisify Model Change (Jue Hou)
+
+ * Fix race condition in error handler test (Miroslav Bajtoš)
+
+ * Travis: drop iojs, add v4.x and v5.x (Miroslav Bajtoš)
+
+ * Correct JSDoc findOrCreate() callback in PersistedModel (Miroslav Bajtoš)
+
+ * Hide verificationToken (Miroslav Bajtoš)
+
+ * test: use ephemeral port for e2e server (Ryan Graham)
+
+ * test: fail on error instead of crash (Ryan Graham)
+
+ * ensure app is booted before integration tests (Ryan Graham)
+
+ * Checkpoint speedup (Amir Jafarian)
+
+ * Pull in API doc fix from PR into master #1910 (crandmck)
+
+
2015-12-22, Version 2.26.2
==========================
@@ -989,8 +1418,6 @@
2014-07-15, Version 2.0.0-beta6
===============================
- * 2.0.0-beta6 (Miroslav Bajtoš)
-
* lib/application: publish Change models to REST API (Miroslav Bajtoš)
* models/change: fix typo (Miroslav Bajtoš)
@@ -1001,8 +1428,6 @@
2014-07-03, Version 2.0.0-beta5
===============================
- * 2.0.0-beta5 (Miroslav Bajtoš)
-
* app: update `url` on `listening` event (Miroslav Bajtoš)
* Fix "ReferenceError: loopback is not defined" in registry.memory(). (Guilherme Cirne)
@@ -1021,8 +1446,6 @@
2014-06-26, Version 2.0.0-beta4
===============================
- * 2.0.0-beta4 (Miroslav Bajtoš)
-
* package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš)
* Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš)
@@ -1149,8 +1572,6 @@
2014-05-28, Version 2.0.0-beta3
===============================
- * 2.0.0-beta3 (Miroslav Bajtoš)
-
* package.json: fix malformed json (Miroslav Bajtoš)
* 2.0.0-beta2 (Ritchie Martori)
diff --git a/Gruntfile.js b/Gruntfile.js
index a7a755c81..4ff9df7f6 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*global module:false*/
module.exports = function(grunt) {
@@ -39,9 +44,6 @@ module.exports = function(grunt) {
common: {
src: ['common/**/*.js']
},
- browser: {
- src: ['browser/**/*.js']
- },
server: {
src: ['server/**/*.js']
},
@@ -54,7 +56,6 @@ module.exports = function(grunt) {
lib: ['lib/**/*.js'],
common: ['common/**/*.js'],
server: ['server/**/*.js'],
- browser: ['browser/**/*.js'],
test: ['test/**/*.js']
},
watch: {
@@ -87,7 +88,8 @@ module.exports = function(grunt) {
src: 'test/*.js',
options: {
reporter: 'dot',
- }
+ require: require.resolve('./test/helpers/use-english.js'),
+ },
},
'unit-xml': {
src: 'test/*.js',
@@ -217,7 +219,14 @@ module.exports = function(grunt) {
grunt.registerTask('e2e-server', function() {
var done = this.async();
var app = require('./test/fixtures/e2e/app');
- app.listen(3000, done);
+ app.listen(0, function() {
+ process.env.PORT = this.address().port;
+ done();
+ });
+ });
+
+ grunt.registerTask('skip-karma-on-windows', function() {
+ console.log('*** SKIPPING PHANTOM-JS BASED TESTS ON WINDOWS ***');
});
grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']);
@@ -229,7 +238,9 @@ module.exports = function(grunt) {
'jscs',
'jshint',
process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit',
- 'karma:unit-once']);
+ process.env.JENKINS_HOME && /^win/.test(process.platform) ?
+ 'skip-karma-on-windows' : 'karma:unit-once',
+ ]);
// alias for sl-ci-run and `npm test`
grunt.registerTask('mocha-and-karma', ['test']);
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..a95641b20
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) IBM Corp. 2013,2016. All Rights Reserved.
+Node module: loopback
+This project is licensed under the MIT License, full text 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
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+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.
diff --git a/LICENSE.md b/LICENSE.md
deleted file mode 100644
index 29d781523..000000000
--- a/LICENSE.md
+++ /dev/null
@@ -1,9 +0,0 @@
-Copyright (c) 2013-2015 StrongLoop, Inc and other contributors.
-
-loopback uses a dual license model.
-
-You may use this library under the terms of the [MIT License][],
-or under the terms of the [StrongLoop Subscription Agreement][].
-
-[MIT License]: http://opensource.org/licenses/MIT
-[StrongLoop Subscription Agreement]: http://strongloop.com/license
diff --git a/README.md b/README.md
index 3ff4a2c3f..54ff6c343 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
[](https://gitter.im/strongloop/loopback?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+**LoopBack 2.x has reached end of life in April 2019. It is no longer supported.**
+
LoopBack is a highly-extensible, open-source Node.js framework that enables you to:
* Create dynamic end-to-end REST APIs with little or no coding.
@@ -73,6 +75,20 @@ StrongLoop provides a number of example applications that illustrate various key
See [loopback-example](https://github.com/strongloop/loopback-example) for details.
+## Module Long Term Support Policy
+
+LoopBack 2.x has reached end of life in April 2019. It is no longer supported.
+
+This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates:
+
+| Version | Status | Published | EOL |
+| ---------- | --------------- | --------- | -------------------- |
+| LoopBack 4 | Current | Oct 2018 | Apr 2021 _(minimum)_ |
+| LoopBack 3 | Maintenance LTS | Dec 2016 | Dec 2020 |
+| LoopBack 2 | End-of-Life | Jul 2014 | Apr 2019 |
+
+Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html).
+
## Resources
* [Documentation](http://docs.strongloop.com/display/LB/LoopBack).
diff --git a/browser/current-context.js b/browser/current-context.js
deleted file mode 100644
index cdf1d8a28..000000000
--- a/browser/current-context.js
+++ /dev/null
@@ -1,10 +0,0 @@
-module.exports = function(loopback) {
- loopback.getCurrentContext = function() {
- return null;
- };
-
- loopback.runInContext =
- loopback.createContext = function() {
- throw new Error('Current context is not supported in the browser.');
- };
-};
diff --git a/common/models/access-token.js b/common/models/access-token.js
index 27cf5206d..38aad7856 100644
--- a/common/models/access-token.js
+++ b/common/models/access-token.js
@@ -1,7 +1,14 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module Dependencies.
*/
+var g = require('strong-globalize')();
+
var loopback = require('../../lib/loopback');
var assert = require('assert');
var uid = require('uid2');
@@ -96,7 +103,7 @@ module.exports = function(AccessToken) {
var id = tokenIdForRequest(req, options);
- if (id) {
+ if (id != null) {
this.findById(id, function(err, token) {
if (err) {
cb(err);
@@ -107,7 +114,7 @@ module.exports = function(AccessToken) {
} else if (isValid) {
cb(null, token);
} else {
- var e = new Error('Invalid Access Token');
+ var e = new Error(g.f('Invalid Access Token'));
e.status = e.statusCode = 401;
e.code = 'INVALID_TOKEN';
cb(e);
@@ -142,11 +149,19 @@ module.exports = function(AccessToken) {
assert(this.ttl, 'token.ttl must exist');
assert(this.ttl >= -1, 'token.ttl must be >= -1');
+ var AccessToken = this.constructor;
+ var userRelation = AccessToken.relations.user; // may not be set up
+ var User = userRelation && userRelation.modelTo;
+
var now = Date.now();
var created = this.created.getTime();
var elapsedSeconds = (now - created) / 1000;
var secondsToLive = this.ttl;
- var isValid = elapsedSeconds < secondsToLive;
+ var eternalTokensAllowed = !!(User && User.settings.allowEternalTokens);
+ var isEternalToken = secondsToLive === -1;
+ var isValid = isEternalToken ?
+ eternalTokensAllowed :
+ elapsedSeconds < secondsToLive;
if (isValid) {
cb(null, isValid);
@@ -194,6 +209,11 @@ module.exports = function(AccessToken) {
if (typeof id === 'string') {
// Add support for oAuth 2.0 bearer token
// http://tools.ietf.org/html/rfc6750
+
+ // To prevent Error: Model::findById requires the id argument
+ // with loopback-datasource-juggler 2.56.0+
+ if (id === '') continue;
+
if (id.indexOf('Bearer ') === 0) {
id = id.substring(7);
// Decode from base64
diff --git a/common/models/access-token.json b/common/models/access-token.json
index a5f360c46..b7ee32f5a 100644
--- a/common/models/access-token.json
+++ b/common/models/access-token.json
@@ -27,12 +27,6 @@
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
- },
- {
- "principalType": "ROLE",
- "principalId": "$everyone",
- "property": "create",
- "permission": "ALLOW"
}
]
}
diff --git a/common/models/acl.js b/common/models/acl.js
index 2a7306b17..96cd79297 100644
--- a/common/models/acl.js
+++ b/common/models/acl.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
Schema ACL options
@@ -31,6 +36,8 @@
*/
+var g = require('strong-globalize')();
+
var loopback = require('../../lib/loopback');
var async = require('async');
var assert = require('assert');
@@ -263,7 +270,7 @@ module.exports = function(ACL) {
* @return {Object[]} An array of ACLs
*/
ACL.getStaticACLs = function getStaticACLs(model, property) {
- var modelClass = loopback.findModel(model);
+ var modelClass = this.registry.findModel(model);
var staticACLs = [];
if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function(acl) {
@@ -355,7 +362,7 @@ module.exports = function(ACL) {
acls = acls.concat(dynACLs);
resolved = self.resolvePermission(acls, req);
if (resolved && resolved.permission === ACL.DEFAULT) {
- var modelClass = loopback.findModel(model);
+ var modelClass = self.registry.findModel(model);
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
}
if (callback) callback(null, resolved);
@@ -387,7 +394,9 @@ module.exports = function(ACL) {
*/
ACL.checkAccessForContext = function(context, callback) {
- var registry = this.registry;
+ var self = this;
+ self.resolveRelatedModels();
+ var roleModel = self.roleModel;
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
@@ -410,11 +419,9 @@ module.exports = function(ACL) {
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
var effectiveACLs = [];
- var staticACLs = this.getStaticACLs(model.modelName, property);
+ var staticACLs = self.getStaticACLs(model.modelName, property);
- var self = this;
- var roleModel = registry.getModelByType(Role);
- this.find({where: {model: model.modelName, property: propertyQuery,
+ self.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function(err, acls) {
if (err) {
if (callback) callback(err);
@@ -500,10 +507,10 @@ module.exports = function(ACL) {
ACL.resolveRelatedModels = function() {
if (!this.roleModel) {
var reg = this.registry;
- this.roleModel = reg.getModelByType(loopback.Role);
- this.roleMappingModel = reg.getModelByType(loopback.RoleMapping);
- this.userModel = reg.getModelByType(loopback.User);
- this.applicationModel = reg.getModelByType(loopback.Application);
+ this.roleModel = reg.getModelByType('Role');
+ this.roleMappingModel = reg.getModelByType('RoleMapping');
+ this.userModel = reg.getModelByType('User');
+ this.applicationModel = reg.getModelByType('Application');
}
};
@@ -530,7 +537,7 @@ module.exports = function(ACL) {
break;
default:
process.nextTick(function() {
- var err = new Error('Invalid principal type: ' + type);
+ var err = new Error(g.f('Invalid principal type: %s', type));
err.statusCode = 400;
cb(err);
});
diff --git a/common/models/application.js b/common/models/application.js
index 617798c15..3286410a5 100644
--- a/common/models/application.js
+++ b/common/models/application.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var assert = require('assert');
var utils = require('../../lib/utils');
diff --git a/common/models/change.js b/common/models/change.js
index be353a039..cf47082dc 100644
--- a/common/models/change.js
+++ b/common/models/change.js
@@ -1,9 +1,17 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module Dependencies.
*/
+var g = require('strong-globalize')();
+
var PersistedModel = require('../../lib/loopback').PersistedModel;
var loopback = require('../../lib/loopback');
+var utils = require('../../lib/utils');
var crypto = require('crypto');
var CJSON = {stringify: require('canonical-json')};
var async = require('async');
@@ -77,6 +85,8 @@ module.exports = function(Change) {
var Change = this;
var errors = [];
+ callback = callback || utils.createPromiseCallback();
+
var tasks = modelIds.map(function(id) {
return function(cb) {
Change.findOrCreateChange(modelName, id, function(err, change) {
@@ -104,13 +114,14 @@ module.exports = function(Change) {
})
.join('\n');
- var msg = 'Cannot rectify ' + modelName + ' changes:\n' + desc;
+ var msg = g.f('Cannot rectify %s changes:\n%s', modelName, desc);
err = new Error(msg);
err.details = { errors: errors };
return callback(err);
}
callback();
});
+ return callback.promise;
};
/**
@@ -137,7 +148,8 @@ module.exports = function(Change) {
*/
Change.findOrCreateChange = function(modelName, modelId, callback) {
- assert(loopback.findModel(modelName), modelName + ' does not exist');
+ assert(this.registry.findModel(modelName), modelName + ' does not exist');
+ callback = callback || utils.createPromiseCallback();
var id = this.idForModel(modelName, modelId);
var Change = this;
@@ -155,6 +167,7 @@ module.exports = function(Change) {
Change.updateOrCreate(ch, callback);
}
});
+ return callback.promise;
};
/**
@@ -171,9 +184,7 @@ module.exports = function(Change) {
change.debug('rectify change');
- cb = cb || function(err) {
- if (err) throw new Error(err);
- };
+ cb = cb || utils.createPromiseCallback();
change.currentRevision(function(err, rev) {
if (err) return cb(err);
@@ -194,6 +205,7 @@ module.exports = function(Change) {
}
);
});
+ return cb.promise;
function doRectify(checkpoint, rev) {
if (rev) {
@@ -248,6 +260,7 @@ module.exports = function(Change) {
*/
Change.prototype.currentRevision = function(cb) {
+ cb = cb || utils.createPromiseCallback();
var model = this.getModelCtor();
var id = this.getModelId();
model.findById(id, function(err, inst) {
@@ -258,6 +271,7 @@ module.exports = function(Change) {
cb(null, null);
}
});
+ return cb.promise;
};
/**
@@ -390,8 +404,11 @@ module.exports = function(Change) {
*/
Change.diff = function(modelName, since, remoteChanges, callback) {
+ callback = callback || utils.createPromiseCallback();
+
if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) {
- return callback(null, {deltas: [], conflicts: []});
+ callback(null, {deltas: [], conflicts: []});
+ return callback.promise;
}
var remoteChangeIndex = {};
var modelIds = [];
@@ -455,6 +472,7 @@ module.exports = function(Change) {
conflicts: conflicts
});
});
+ return callback.promise;
};
/**
diff --git a/common/models/checkpoint.js b/common/models/checkpoint.js
index 304f83148..b48cb1866 100644
--- a/common/models/checkpoint.js
+++ b/common/models/checkpoint.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/**
* Module Dependencies.
*/
@@ -27,43 +32,45 @@ module.exports = function(Checkpoint) {
* Get the current checkpoint id
* @callback {Function} callback
* @param {Error} err
- * @param {Number} checkpointId The current checkpoint id
+ * @param {Number} checkpoint The current checkpoint seq
*/
-
Checkpoint.current = function(cb) {
var Checkpoint = this;
- this.find({
- limit: 1,
- order: 'seq DESC'
- }, function(err, checkpoints) {
- if (err) return cb(err);
- var checkpoint = checkpoints[0];
- if (checkpoint) {
- cb(null, checkpoint.seq);
- } else {
- Checkpoint.create({ seq: 1 }, function(err, checkpoint) {
- if (err) return cb(err);
- cb(null, checkpoint.seq);
- });
- }
+ Checkpoint._getSingleton(function(err, cp) {
+ cb(err, cp.seq);
});
};
- Checkpoint.observe('before save', function(ctx, next) {
- if (!ctx.instance) {
- // Example: Checkpoint.updateAll() and Checkpoint.updateOrCreate()
- return next(new Error('Checkpoint does not support partial updates.'));
- }
+ Checkpoint._getSingleton = function(cb) {
+ var query = {limit: 1}; // match all instances, return only one
+ var initialData = {seq: 1};
+ this.findOrCreate(query, initialData, cb);
+ };
- var model = ctx.instance;
- if (!model.getId() && model.seq === undefined) {
- model.constructor.current(function(err, seq) {
- if (err) return next(err);
- model.seq = seq + 1;
- next();
+ /**
+ * Increase the current checkpoint if it already exists otherwise initialize it
+ * @callback {Function} callback
+ * @param {Error} err
+ * @param {Object} checkpoint The current checkpoint
+ */
+ Checkpoint.bumpLastSeq = function(cb) {
+ var Checkpoint = this;
+ Checkpoint._getSingleton(function(err, cp) {
+ if (err) return cb(err);
+ var originalSeq = cp.seq;
+ cp.seq++;
+ // Update the checkpoint but only if it was not changed under our hands
+ Checkpoint.updateAll({id: cp.id, seq: originalSeq}, {seq: cp.seq}, function(err, info) {
+ if (err) return cb(err);
+ // possible outcomes
+ // 1) seq was updated to seq+1 - exactly what we wanted!
+ // 2) somebody else already updated seq to seq+1 and our call was a no-op.
+ // That should be ok, checkpoints are time based, so we reuse the one created just now
+ // 3) seq was bumped more than once, so we will be using a value that is behind the latest seq.
+ // @bajtos is not entirely sure if this is ok, but since it wasn't handled by the current implementation either,
+ // he thinks we can keep it this way.
+ cb(null, cp);
});
- } else {
- next();
- }
- });
+ });
+ };
};
diff --git a/common/models/email.js b/common/models/email.js
index f73628027..8ead34aa6 100644
--- a/common/models/email.js
+++ b/common/models/email.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/**
* Email model. Extends LoopBack base [Model](#model-new-model).
* @property {String} to Email addressee. Required.
@@ -10,6 +15,8 @@
* @inherits {Model}
*/
+var g = require('strong-globalize')();
+
module.exports = function(Email) {
/**
@@ -39,13 +46,13 @@ module.exports = function(Email) {
*/
Email.send = function() {
- throw new Error('You must connect the Email Model to a Mail connector');
+ throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
};
/**
* A shortcut for Email.send(this).
*/
Email.prototype.send = function() {
- throw new Error('You must connect the Email Model to a Mail connector');
+ throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
};
};
diff --git a/common/models/key-value-model.js b/common/models/key-value-model.js
new file mode 100644
index 000000000..2c0aeed78
--- /dev/null
+++ b/common/models/key-value-model.js
@@ -0,0 +1,227 @@
+var g = require('strong-globalize')();
+
+/**
+ * Data model for key-value databases.
+ *
+ * @class KeyValueModel
+ * @inherits {Model}
+ */
+
+module.exports = function(KeyValueModel) {
+ /**
+ * Return the value associated with a given key.
+ *
+ * @param {String} key Key to use when searching the database.
+ * @options {Object} options
+ * @callback {Function} callback
+ * @param {Error} err Error object.
+ * @param {Any} result Value associated with the given key.
+ * @promise
+ *
+ * @header KeyValueModel.get(key, cb)
+ */
+ KeyValueModel.get = function(key, options, callback) {
+ throwNotAttached(this.modelName, 'get');
+ };
+
+ /**
+ * Persist a value and associate it with the given key.
+ *
+ * @param {String} key Key to associate with the given value.
+ * @param {Any} value Value to persist.
+ * @options {Number|Object} options Optional settings for the key-value
+ * pair. If a Number is provided, it is set as the TTL (time to live) in ms
+ * (milliseconds) for the key-value pair.
+ * @property {Number} ttl TTL for the key-value pair in ms.
+ * @callback {Function} callback
+ * @param {Error} err Error object.
+ * @promise
+ *
+ * @header KeyValueModel.set(key, value, cb)
+ */
+ KeyValueModel.set = function(key, value, options, callback) {
+ throwNotAttached(this.modelName, 'set');
+ };
+
+ /**
+ * Set the TTL (time to live) in ms (milliseconds) for a given key. TTL is the
+ * remaining time before a key-value pair is discarded from the database.
+ *
+ * @param {String} key Key to use when searching the database.
+ * @param {Number} ttl TTL in ms to set for the key.
+ * @options {Object} options
+ * @callback {Function} callback
+ * @param {Error} err Error object.
+ * @promise
+ *
+ * @header KeyValueModel.expire(key, ttl, cb)
+ */
+ KeyValueModel.expire = function(key, ttl, options, callback) {
+ throwNotAttached(this.modelName, 'expire');
+ };
+
+ /**
+ * Return the TTL (time to live) for a given key. TTL is the remaining time
+ * before a key-value pair is discarded from the database.
+ *
+ * @param {String} key Key to use when searching the database.
+ * @options {Object} options
+ * @callback {Function} callback
+ * @param {Error} error
+ * @param {Number} ttl Expiration time for the key-value pair. `undefined` if
+ * TTL was not initially set.
+ * @promise
+ *
+ * @header KeyValueModel.ttl(key, cb)
+ */
+ KeyValueModel.ttl = function(key, options, callback) {
+ throwNotAttached(this.modelName, 'ttl');
+ };
+
+ /**
+ * Return all keys in the database.
+ *
+ * **WARNING**: This method is not suitable for large data sets as all
+ * key-values pairs are loaded into memory at once. For large data sets,
+ * use `iterateKeys()` instead.
+ *
+ * @param {Object} filter An optional filter object with the following
+ * @param {String} filter.match Glob string used to filter returned
+ * keys (i.e. `userid.*`). All connectors are required to support `*` and
+ * `?`, but may also support additional special characters specific to the
+ * database.
+ * @param {Object} options
+ * @callback {Function} callback
+ * @promise
+ *
+ * @header KeyValueModel.keys(filter, cb)
+ */
+ KeyValueModel.keys = function(filter, options, callback) {
+ throwNotAttached(this.modelName, 'keys');
+ };
+
+ /**
+ * Asynchronously iterate all keys in the database. Similar to `.keys()` but
+ * instead allows for iteration over large data sets without having to load
+ * everything into memory at once.
+ *
+ * Callback example:
+ * ```js
+ * // Given a model named `Color` with two keys `red` and `blue`
+ * var iterator = Color.iterateKeys();
+ * it.next(function(err, key) {
+ * // key contains `red`
+ * it.next(function(err, key) {
+ * // key contains `blue`
+ * });
+ * });
+ * ```
+ *
+ * Promise example:
+ * ```js
+ * // Given a model named `Color` with two keys `red` and `blue`
+ * var iterator = Color.iterateKeys();
+ * Promise.resolve().then(function() {
+ * return it.next();
+ * })
+ * .then(function(key) {
+ * // key contains `red`
+ * return it.next();
+ * });
+ * .then(function(key) {
+ * // key contains `blue`
+ * });
+ * ```
+ *
+ * @param {Object} filter An optional filter object with the following
+ * @param {String} filter.match Glob string to use to filter returned
+ * keys (i.e. `userid.*`). All connectors are required to support `*` and
+ * `?`. They may also support additional special characters that are
+ * specific to the backing database.
+ * @param {Object} options
+ * @returns {AsyncIterator} An Object implementing `next(cb) -> Promise`
+ * function that can be used to iterate all keys.
+ *
+ * @header KeyValueModel.iterateKeys(filter)
+ */
+ KeyValueModel.iterateKeys = function(filter, options) {
+ throwNotAttached(this.modelName, 'iterateKeys');
+ };
+
+ /*!
+ * Set up remoting metadata for this model.
+ *
+ * **Notes**:
+ * - The method is called automatically by `Model.extend` and/or
+ * `app.registry.createModel`
+ * - In general, base models use call this to ensure remote methods are
+ * inherited correctly, see bug at
+ * https://github.com/strongloop/loopback/issues/2350
+ */
+ KeyValueModel.setup = function() {
+ KeyValueModel.base.setup.apply(this, arguments);
+
+ this.remoteMethod('get', {
+ accepts: {
+ arg: 'key', type: 'string', required: true,
+ http: { source: 'path' },
+ },
+ returns: { arg: 'value', type: 'any', root: true },
+ http: { path: '/:key', verb: 'get' },
+ rest: { after: convertNullToNotFoundError },
+ });
+
+ this.remoteMethod('set', {
+ accepts: [
+ { arg: 'key', type: 'string', required: true,
+ http: { source: 'path' }},
+ { arg: 'value', type: 'any', required: true,
+ http: { source: 'body' }},
+ { arg: 'ttl', type: 'number',
+ http: { source: 'query' },
+ description: 'time to live in milliseconds' },
+ ],
+ http: { path: '/:key', verb: 'put' },
+ });
+
+ this.remoteMethod('expire', {
+ accepts: [
+ { arg: 'key', type: 'string', required: true,
+ http: { source: 'path' }},
+ { arg: 'ttl', type: 'number', required: true,
+ http: { source: 'form' }},
+ ],
+ http: { path: '/:key/expire', verb: 'put' },
+ });
+
+ this.remoteMethod('keys', {
+ accepts: {
+ arg: 'filter', type: 'object', required: false,
+ http: { source: 'query' },
+ },
+ returns: { arg: 'keys', type: ['string'], root: true },
+ http: { path: '/keys', verb: 'get' },
+ });
+ };
+};
+
+function throwNotAttached(modelName, methodName) {
+ throw new Error(g.f(
+ 'Cannot call %s.%s(). ' +
+ 'The %s method has not been setup. ' +
+ 'The {{KeyValueModel}} has not been correctly attached ' +
+ 'to a {{DataSource}}!',
+ modelName, methodName, methodName));
+}
+
+function convertNullToNotFoundError(ctx, cb) {
+ if (ctx.result !== null) return cb();
+
+ var modelName = ctx.method.sharedClass.name;
+ var id = ctx.getArgByName('id');
+ var msg = g.f('Unknown "%s" {{key}} "%s".', modelName, id);
+ var error = new Error(msg);
+ error.statusCode = error.status = 404;
+ error.code = 'KEY_NOT_FOUND';
+ cb(error);
+}
diff --git a/common/models/key-value-model.json b/common/models/key-value-model.json
new file mode 100644
index 000000000..72884e879
--- /dev/null
+++ b/common/models/key-value-model.json
@@ -0,0 +1,4 @@
+{
+ "name": "KeyValueModel",
+ "base": "Model"
+}
diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js
index ee728483c..53af71f9a 100644
--- a/common/models/role-mapping.js
+++ b/common/models/role-mapping.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../../lib/loopback');
/**
diff --git a/common/models/role-mapping.json b/common/models/role-mapping.json
index 592f29064..732e24af3 100644
--- a/common/models/role-mapping.json
+++ b/common/models/role-mapping.json
@@ -11,7 +11,10 @@
"type": "string",
"description": "The principal type, such as user, application, or role"
},
- "principalId": "string"
+ "principalId": {
+ "type": "string",
+ "index": true
+ }
},
"relations": {
"role": {
diff --git a/common/models/role.js b/common/models/role.js
index a176fec17..a09f7563b 100644
--- a/common/models/role.js
+++ b/common/models/role.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../../lib/loopback');
var debug = require('debug')('loopback:security:role');
var assert = require('assert');
@@ -31,9 +36,9 @@ module.exports = function(Role) {
Role.resolveRelatedModels = function() {
if (!this.userModel) {
var reg = this.registry;
- this.roleMappingModel = reg.getModelByType(loopback.RoleMapping);
- this.userModel = reg.getModelByType(loopback.User);
- this.applicationModel = reg.getModelByType(loopback.Application);
+ this.roleMappingModel = reg.getModelByType('RoleMapping');
+ this.userModel = reg.getModelByType('User');
+ this.applicationModel = reg.getModelByType('Application');
}
};
@@ -75,26 +80,27 @@ module.exports = function(Role) {
};
var model = relsToModels[rel];
- listByPrincipalType(model, relsToTypes[rel], query, callback);
+ listByPrincipalType(this, model, relsToTypes[rel], query, callback);
};
});
/**
* Fetch all models assigned to this role
* @private
+ * @param {object} Context role context
* @param {*} model model type to fetch
* @param {String} [principalType] principalType used in the rolemapping for model
* @param {object} [query] query object passed to model find call
* @param {Function} [callback] callback function called with `(err, models)` arguments.
*/
- function listByPrincipalType(model, principalType, query, callback) {
+ function listByPrincipalType(context, model, principalType, query, callback) {
if (callback === undefined) {
callback = query;
query = {};
}
roleModel.roleMappingModel.find({
- where: {roleId: this.id, principalType: principalType}
+ where: {roleId: context.id, principalType: principalType},
}, function(err, mappings) {
var ids;
if (err) {
@@ -123,8 +129,9 @@ module.exports = function(Role) {
/**
* Add custom handler for roles.
* @param {String} role Name of role.
- * @param {Function} resolver Function that determines if a principal is in the specified role.
- * Signature must be `function(role, context, callback)`
+ * @param {Function} resolver Function that determines
+ * if a principal is in the specified role.
+ * Should provide a callback or return a promise.
*/
Role.registerResolver = function(role, resolver) {
if (!Role.resolvers) {
@@ -147,12 +154,10 @@ module.exports = function(Role) {
});
function isUserClass(modelClass) {
- if (modelClass) {
- return modelClass === loopback.User ||
- modelClass.prototype instanceof loopback.User;
- } else {
- return false;
- }
+ if (!modelClass) return false;
+ var User = modelClass.modelBuilder.models.User;
+ if (!User) return false;
+ return modelClass == User || modelClass.prototype instanceof User;
}
/*!
@@ -292,7 +297,14 @@ module.exports = function(Role) {
var resolver = Role.resolvers[role];
if (resolver) {
debug('Custom resolver found for role %s', role);
- resolver(role, context, callback);
+
+ var promise = resolver(role, context, callback);
+ if (promise && typeof promise.then === 'function') {
+ promise.then(
+ function(result) { callback(null, result); },
+ callback
+ );
+ }
return;
}
@@ -371,7 +383,12 @@ module.exports = function(Role) {
* @param {Error} err Error object.
* @param {String[]} roles An array of role IDs
*/
- Role.getRoles = function(context, callback) {
+ Role.getRoles = function(context, options, callback) {
+ if (!callback && typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
@@ -421,15 +438,24 @@ module.exports = function(Role) {
if (principalType && principalId) {
// Please find() treat undefined matches all values
inRoleTasks.push(function(done) {
- roleMappingModel.find({where: {principalType: principalType,
- principalId: principalId}}, function(err, mappings) {
+ var filter = {where: {principalType: principalType, principalId: principalId}};
+ if (options.returnOnlyRoleNames === true) {
+ filter.include = ['role'];
+ }
+ roleMappingModel.find(filter, function(err, mappings) {
debug('Role mappings found: %s %j', err, mappings);
if (err) {
if (done) done(err);
return;
}
mappings.forEach(function(m) {
- addRole(m.roleId);
+ var role;
+ if (options.returnOnlyRoleNames === true) {
+ role = m.toJSON().role.name;
+ } else {
+ role = m.roleId;
+ }
+ addRole(role);
});
if (done) done();
});
@@ -442,4 +468,6 @@ module.exports = function(Role) {
if (callback) callback(err, roles);
});
};
+
+ Role.validatesUniquenessOf('name', { message: 'already exists' });
};
diff --git a/common/models/scope.js b/common/models/scope.js
index 3c713b535..478124d2a 100644
--- a/common/models/scope.js
+++ b/common/models/scope.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var assert = require('assert');
var loopback = require('../../lib/loopback');
diff --git a/common/models/user.js b/common/models/user.js
index b91b3ca18..73bbda792 100644
--- a/common/models/user.js
+++ b/common/models/user.js
@@ -1,13 +1,22 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module Dependencies.
*/
+var g = require('strong-globalize')();
+
+var isEmail = require('isemail');
var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils');
var path = require('path');
+var qs = require('querystring');
var SALT_WORK_FACTOR = 10;
var crypto = require('crypto');
-
+var MAX_PASSWORD_LENGTH = 72;
var bcrypt;
try {
// Try the native module first
@@ -199,23 +208,44 @@ module.exports = function(User) {
var query = self.normalizeCredentials(credentials, realmRequired,
realmDelimiter);
- if (realmRequired && !query.realm) {
- var err1 = new Error('realm is required');
- err1.statusCode = 400;
- err1.code = 'REALM_REQUIRED';
- fn(err1);
- return fn.promise;
+ if (realmRequired) {
+ if (!query.realm) {
+ var err1 = new Error(g.f('{{realm}} is required'));
+ err1.statusCode = 400;
+ err1.code = 'REALM_REQUIRED';
+ fn(err1);
+ return fn.promise;
+ } else if (typeof query.realm !== 'string') {
+ var err5 = new Error(g.f('Invalid realm'));
+ err5.statusCode = 400;
+ err5.code = 'INVALID_REALM';
+ fn(err5);
+ return fn.promise;
+ }
}
if (!query.email && !query.username) {
- var err2 = new Error('username or email is required');
+ var err2 = new Error(g.f('{{username}} or {{email}} is required'));
err2.statusCode = 400;
err2.code = 'USERNAME_EMAIL_REQUIRED';
fn(err2);
return fn.promise;
}
+ if (query.username && typeof query.username !== 'string') {
+ var err3 = new Error(g.f('Invalid username'));
+ err3.statusCode = 400;
+ err3.code = 'INVALID_USERNAME';
+ fn(err3);
+ return fn.promise;
+ } else if (query.email && typeof query.email !== 'string') {
+ var err4 = new Error(g.f('Invalid email'));
+ err4.statusCode = 400;
+ err4.code = 'INVALID_EMAIL';
+ fn(err4);
+ return fn.promise;
+ }
self.findOne({where: query}, function(err, user) {
- var defaultError = new Error('login failed');
+ var defaultError = new Error(g.f('login failed'));
defaultError.statusCode = 401;
defaultError.code = 'LOGIN_FAILED';
@@ -245,7 +275,7 @@ module.exports = function(User) {
if (self.settings.emailVerificationRequired && !user.emailVerified) {
// Fail to log in if email verification is not done yet
debug('User email has not been verified');
- err = new Error('login failed as the email has not been verified');
+ err = new Error(g.f('login failed as the email has not been verified'));
err.statusCode = 401;
err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED';
fn(err);
@@ -285,23 +315,49 @@ module.exports = function(User) {
User.logout = function(tokenId, fn) {
fn = fn || utils.createPromiseCallback();
- this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) {
+
+ if (!tokenId) {
+ var err = new Error(g.f('{{accessToken}} is required to logout'));
+ err.status = 401;
+ process.nextTick(function() { fn(err); });
+ return fn.promise;
+ }
+
+ this.relations.accessTokens.modelTo.destroyById(tokenId, function(err, info) {
if (err) {
fn(err);
- } else if (accessToken) {
- accessToken.destroy(fn);
+ } else if ('count' in info && info.count === 0) {
+ err = new Error(g.f('Could not find {{accessToken}}'));
+ err.status = 401;
+ fn(err);
} else {
- fn(new Error('could not find accessToken'));
+ fn();
}
});
return fn.promise;
};
+ User.observe('before delete', function(ctx, next) {
+ var AccessToken = ctx.Model.relations.accessTokens.modelTo;
+ var pkName = ctx.Model.definition.idName() || 'id';
+ ctx.Model.find({ where: ctx.where, fields: [pkName] }, function(err, list) {
+ if (err) return next(err);
+
+ var ids = list.map(function(u) { return u[pkName]; });
+ ctx.where = {};
+ ctx.where[pkName] = { inq: ids };
+
+ AccessToken.destroyAll({ userId: { inq: ids }}, next);
+ });
+ });
+
/**
* Compare the given `password` with the users hashed password.
*
* @param {String} password The plain text password
- * @returns {Boolean}
+ * @callback {Function} callback Callback function
+ * @param {Error} err Error object
+ * @param {Boolean} isMatch Returns true if the given `password` matches record
*/
User.prototype.hasPassword = function(plain, fn) {
@@ -341,6 +397,10 @@ module.exports = function(User) {
* @property {String} text Text of email.
* @property {String} template Name of template that displays verification
* page, for example, `'verify.ejs'.
+ * @property {Function} templateFn A function generating the email HTML body
+ * from `verify()` options object and generated attributes like `options.verifyHref`.
+ * It must accept the option object and a callback function with `(err, html)`
+ * as parameters
* @property {String} redirect Page to which user will be redirected after
* they verify their email, for example `'/'` for root URI.
* @property {Function} generateVerificationToken A function to be used to
@@ -356,6 +416,7 @@ module.exports = function(User) {
var user = this;
var userModel = this.constructor;
var registry = userModel.registry;
+ var pkName = userModel.definition.idName() || 'id';
assert(typeof options === 'object', 'options required when calling user.verify()');
assert(options.type, 'You must supply a verification type (options.type)');
assert(options.type === 'email', 'Unsupported verification type');
@@ -377,18 +438,24 @@ module.exports = function(User) {
(options.protocol === 'https' && options.port == '443')
) ? '' : ':' + options.port;
+ var urlPath = joinUrlPath(
+ options.restApiRoot,
+ userModel.http.path,
+ userModel.sharedClass.find('confirm', true).http.path
+ );
+
options.verifyHref = options.verifyHref ||
options.protocol +
'://' +
options.host +
displayPort +
- options.restApiRoot +
- userModel.http.path +
- userModel.sharedClass.find('confirm', true).http.path +
- '?uid=' +
- options.user.id +
- '&redirect=' +
- options.redirect;
+ urlPath +
+ '?' + qs.stringify({
+ uid: '' + options.user[pkName],
+ redirect: options.redirect,
+ });
+
+ options.templateFn = options.templateFn || createVerificationEmailBody;
// Email model
var Email = options.mailer || this.constructor.email || registry.getModelByType(loopback.Email);
@@ -413,30 +480,50 @@ module.exports = function(User) {
function sendEmail(user) {
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 || g.f('Please verify your email by opening ' +
+ 'this link in a web browser:\n\t%s', options.verifyHref);
- options.text = options.text.replace('{href}', options.verifyHref);
+ options.text = options.text.replace(/\{href\}/g, options.verifyHref);
options.to = options.to || user.email;
- options.subject = options.subject || 'Thanks for Registering';
+ options.subject = options.subject || g.f('Thanks for Registering');
options.headers = options.headers || {};
- var template = loopback.template(options.template);
- options.html = template(options);
-
- Email.send(options, function(err, email) {
+ options.templateFn(options, function(err, html) {
if (err) {
fn(err);
} else {
- fn(null, {email: email, token: user.verificationToken, uid: user.id});
+ setHtmlContentAndSend(html);
}
});
+
+ function setHtmlContentAndSend(html) {
+ options.html = html;
+
+ // Remove options.template to prevent rejection by certain
+ // nodemailer transport plugins.
+ delete options.template;
+
+ Email.send(options, function(err, email) {
+ if (err) {
+ fn(err);
+ } else {
+ fn(null, {email: email, token: user.verificationToken, uid: user[pkName]});
+ }
+ });
+ }
}
return fn.promise;
};
+ function createVerificationEmailBody(options, cb) {
+ var template = loopback.template(options.template);
+ var body = template(options);
+ cb(null, body);
+ }
+
/**
* A default verification token generator which accepts the user the token is
* being generated for and a callback function to indicate completion.
@@ -469,7 +556,7 @@ module.exports = function(User) {
fn(err);
} else {
if (user && user.verificationToken === token) {
- user.verificationToken = undefined;
+ user.verificationToken = null;
user.emailVerified = true;
user.save(function(err) {
if (err) {
@@ -480,11 +567,11 @@ module.exports = function(User) {
});
} else {
if (user) {
- err = new Error('Invalid token: ' + token);
+ err = new Error(g.f('Invalid token: %s', token));
err.statusCode = 400;
err.code = 'INVALID_TOKEN';
} else {
- err = new Error('User not found: ' + uid);
+ err = new Error(g.f('User not found: %s', uid));
err.statusCode = 404;
err.code = 'USER_NOT_FOUND';
}
@@ -496,11 +583,12 @@ module.exports = function(User) {
};
/**
- * Create a short lived acess token for temporary login. Allows users
+ * Create a short lived access token for temporary login. Allows users
* to change passwords if forgotten.
*
* @options {Object} options
- * @prop {String} email The user's email address
+ * @property {String} email The user's email address
+ * @property {String} realm The user's realm (optional)
* @callback {Function} callback
* @param {Error} err
*/
@@ -509,29 +597,48 @@ module.exports = function(User) {
cb = cb || utils.createPromiseCallback();
var UserModel = this;
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
-
options = options || {};
if (typeof options.email !== 'string') {
- var err = new Error('Email is required');
+ var err = new Error(g.f('Email is required'));
err.statusCode = 400;
err.code = 'EMAIL_REQUIRED';
cb(err);
return cb.promise;
}
- UserModel.findOne({ where: {email: options.email} }, function(err, user) {
+ try {
+ if (options.password) {
+ UserModel.validatePassword(options.password);
+ }
+ } catch (err) {
+ return cb(err);
+ }
+ var where = {
+ email: options.email
+ };
+ if (options.realm) {
+ where.realm = options.realm;
+ }
+ UserModel.findOne({ where: where }, function(err, user) {
if (err) {
return cb(err);
}
if (!user) {
- err = new Error('Email not found');
+ err = new Error(g.f('Email not found'));
err.statusCode = 404;
err.code = 'EMAIL_NOT_FOUND';
return cb(err);
}
// create a short lived access token for temp login to change password
// TODO(ritch) - eventually this should only allow password change
- user.accessTokens.create({ttl: ttl}, function(err, accessToken) {
+ if (UserModel.settings.emailVerificationRequired && !user.emailVerified) {
+ err = new Error(g.f('Email has not been verified'));
+ err.statusCode = 401;
+ err.code = 'RESET_FAILED_EMAIL_NOT_VERIFIED';
+ return cb(err);
+ }
+
+ user.createAccessToken(ttl, function(err, accessToken) {
if (err) {
return cb(err);
}
@@ -539,7 +646,8 @@ module.exports = function(User) {
UserModel.emit('resetPasswordRequest', {
email: options.email,
accessToken: accessToken,
- user: user
+ user: user,
+ options: options,
});
});
});
@@ -557,14 +665,45 @@ module.exports = function(User) {
};
User.validatePassword = function(plain) {
- if (typeof plain === 'string' && plain) {
+ var err;
+ if (plain && typeof plain === 'string' && plain.length <= MAX_PASSWORD_LENGTH) {
return true;
}
- var err = new Error('Invalid password: ' + plain);
+ if (plain.length > MAX_PASSWORD_LENGTH) {
+ err = new Error(g.f('Password too long: %s', plain));
+ err.code = 'PASSWORD_TOO_LONG';
+ } else {
+ err = new Error(g.f('Invalid password: %s', plain));
+ err.code = 'INVALID_PASSWORD';
+ }
err.statusCode = 422;
throw err;
};
+ User._invalidateAccessTokensOfUsers = function(userIds, options, cb) {
+ if (typeof options === 'function' && cb === undefined) {
+ cb = options;
+ options = {};
+ }
+
+ if (!Array.isArray(userIds) || !userIds.length)
+ return process.nextTick(cb);
+
+ var accessTokenRelation = this.relations.accessTokens;
+ if (!accessTokenRelation)
+ return process.nextTick(cb);
+
+ var AccessToken = accessTokenRelation.modelTo;
+
+ var query = {userId: {inq: userIds}};
+ var tokenPK = AccessToken.definition.idName() || 'id';
+ if (options.accessToken && tokenPK in options.accessToken) {
+ query[tokenPK] = {neq: options.accessToken[tokenPK]};
+ }
+
+ AccessToken.deleteAll(query, options, cb);
+ };
+
/*!
* Setup an extended user model.
*/
@@ -599,14 +738,6 @@ module.exports = function(User) {
}
};
- // Access token to normalize email credentials
- UserModel.observe('access', function normalizeEmailCase(ctx, next) {
- if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where && ctx.query.where.email) {
- ctx.query.where.email = ctx.query.where.email.toLowerCase();
- }
- next();
- });
-
// Make sure emailVerified is not set by creation
UserModel.beforeRemote('create', function(ctx, user, next) {
var body = ctx.req.body;
@@ -622,17 +753,18 @@ module.exports = function(User) {
description: 'Login a user with username/email and password.',
accepts: [
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
- {arg: 'include', type: ['string'], http: {source: 'query' },
+ {arg: 'include', type: ['string'], http: {source: 'query'},
description: 'Related objects to include in the response. ' +
- 'See the description of return value for more details.'}
+ 'See the description of return value for more details.' },
],
returns: {
arg: 'accessToken', type: 'object', root: true,
description:
- 'The response body contains properties of the AccessToken created on login.\n' +
+ g.f('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'
+ ' - `user` - `U+007BUserU+007D` - Data of the currently logged in user. ' +
+ '{{(`include=user`)}}\n\n'),
},
http: {verb: 'post'}
}
@@ -643,15 +775,14 @@ module.exports = function(User) {
{
description: 'Logout a user with access token.',
accepts: [
- {arg: 'access_token', type: 'string', required: true, http: function(ctx) {
- var req = ctx && ctx.req;
- var accessToken = req && req.accessToken;
- var tokenID = accessToken && accessToken.id;
-
- return tokenID;
- }, description: 'Do not supply this argument, it is automatically extracted ' +
- 'from request headers.'
- }
+ {arg: 'access_token', type: 'string', http: function(ctx) {
+ var req = ctx && ctx.req;
+ var accessToken = req && req.accessToken;
+ var tokenID = accessToken ? accessToken.id : undefined;
+ return tokenID;
+ }, description: 'Do not supply this argument, it is automatically extracted ' +
+ 'from request headers.',
+ },
],
http: {verb: 'all'}
}
@@ -684,7 +815,7 @@ module.exports = function(User) {
UserModel.afterRemote('confirm', function(ctx, inst, next) {
if (ctx.args.redirect !== undefined) {
if (!ctx.res) {
- return next(new Error('The transport does not support HTTP redirects.'));
+ return next(new Error(g.f('The transport does not support HTTP redirects.')));
}
ctx.res.location(ctx.args.redirect);
ctx.res.status(302);
@@ -699,10 +830,9 @@ module.exports = function(User) {
assert(loopback.AccessToken, 'AccessToken model must be defined before User model');
UserModel.accessToken = loopback.AccessToken;
- // email validation regex
- var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-
- UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
+ UserModel.validate('email', emailValidator, {
+ message: g.f('Must provide a valid email')
+ });
// FIXME: We need to add support for uniqueness of composite keys in juggler
if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) {
@@ -710,6 +840,34 @@ module.exports = function(User) {
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
}
+ UserModel.once('attached', function() {
+ if (UserModel.app.get('logoutSessionsOnSensitiveChanges') !== undefined)
+ return;
+
+ g.warn([
+ '',
+ 'The user model %j is attached to an application that does not specify',
+ 'whether other sessions should be invalidated when a password or',
+ 'an email has changed. Session invalidation is important for security',
+ 'reasons as it allows users to recover from various account breach',
+ 'situations.',
+ '',
+ 'We recommend turning this feature on by setting',
+ '"{{logoutSessionsOnSensitiveChanges}}" to {{true}} in',
+ '{{server/config.json}} (unless you have implemented your own solution',
+ 'for token invalidation).',
+ '',
+ 'We also recommend enabling "{{injectOptionsFromRemoteContext}}" in',
+ '%s\'s settings (typically via common/models/*.json file).',
+ 'This setting is required for the invalidation algorithm to keep ',
+ 'the current session valid.',
+ '',
+ 'Learn more in our documentation at',
+ 'https://loopback.io/doc/en/lb2/AccessToken-invalidation.html',
+ '',
+ ].join('\n'), UserModel.modelName, UserModel.modelName);
+ });
+
return UserModel;
};
@@ -719,4 +877,112 @@ module.exports = function(User) {
User.setup();
+ // --- OPERATION HOOKS ---
+ //
+ // Important: Operation hooks are inherited by subclassed models,
+ // therefore they must be registered outside of setup() function
+
+ // Access token to normalize email credentials
+ User.observe('access', function normalizeEmailCase(ctx, next) {
+ if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where &&
+ ctx.query.where.email && typeof(ctx.query.where.email) === 'string') {
+ ctx.query.where.email = ctx.query.where.email.toLowerCase();
+ }
+ next();
+ });
+
+ User.observe('before save', function prepareForTokenInvalidation(ctx, next) {
+ var invalidationEnabled = ctx.Model.app &&
+ ctx.Model.app.get('logoutSessionsOnSensitiveChanges');
+ if (!invalidationEnabled) return next();
+
+ if (ctx.isNewInstance) return next();
+ if (!ctx.where && !ctx.instance) return next();
+
+ var pkName = ctx.Model.definition.idName() || 'id';
+
+ var where = ctx.where;
+ if (!where) {
+ where = {};
+ where[pkName] = ctx.instance[pkName];
+ }
+
+ ctx.Model.find({where: where}, ctx.options, function(err, userInstances) {
+ if (err) return next(err);
+ ctx.hookState.originalUserData = userInstances.map(function(u) {
+ var user = {};
+ user[pkName] = u[pkName];
+ user.email = u.email;
+ user.password = u.password;
+ return user;
+ });
+ var emailChanged;
+ if (ctx.instance) {
+ // Check if map does not return an empty array
+ // Fix server crashes when try to PUT a non existent id
+ if (ctx.hookState.originalUserData.length > 0) {
+ emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email;
+ } else {
+ emailChanged = true;
+ }
+
+ if (emailChanged && ctx.Model.settings.emailVerificationRequired) {
+ ctx.instance.emailVerified = false;
+ }
+ } else if (ctx.data.email) {
+ emailChanged = ctx.hookState.originalUserData.some(function(data) {
+ return data.email != ctx.data.email;
+ });
+ if (emailChanged && ctx.Model.settings.emailVerificationRequired) {
+ ctx.data.emailVerified = false;
+ }
+ }
+
+ next();
+ });
+ });
+
+ User.observe('after save', function invalidateOtherTokens(ctx, next) {
+ var invalidationEnabled = ctx.Model.app &&
+ ctx.Model.app.get('logoutSessionsOnSensitiveChanges');
+ if (!invalidationEnabled) return next();
+
+ if (!ctx.instance && !ctx.data) return next();
+ if (!ctx.hookState.originalUserData) return next();
+
+ var pkName = ctx.Model.definition.idName() || 'id';
+ var newEmail = (ctx.instance || ctx.data).email;
+ var newPassword = (ctx.instance || ctx.data).password;
+
+ if (!newEmail && !newPassword) return next();
+
+ var userIdsToExpire = ctx.hookState.originalUserData.filter(function(u) {
+ return (newEmail && u.email !== newEmail) ||
+ (newPassword && u.password !== newPassword);
+ }).map(function(u) {
+ return u[pkName];
+ });
+ ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, ctx.options, next);
+ });
};
+
+function emailValidator(err, done) {
+ var value = this.email;
+ if (value == null)
+ return;
+ if (typeof value !== 'string')
+ return err('string');
+ if (value === '') return;
+ if (!isEmail(value))
+ return err('email');
+}
+
+function joinUrlPath(args) {
+ var result = arguments[0];
+ for (var ix = 1; ix < arguments.length; ix++) {
+ var next = arguments[ix];
+ result += result[result.length - 1] === '/' && next[0] === '/' ?
+ next.slice(1) : next;
+ }
+ return result;
+}
diff --git a/common/models/user.json b/common/models/user.json
index d70a89d3f..a3f3973fc 100644
--- a/common/models/user.json
+++ b/common/models/user.json
@@ -32,7 +32,7 @@
"options": {
"caseSensitiveEmail": true
},
- "hidden": ["password"],
+ "hidden": ["password", "verificationToken"],
"acls": [
{
"principalType": "ROLE",
@@ -75,6 +75,12 @@
"permission": "ALLOW",
"property": "updateAttributes"
},
+ {
+ "principalType": "ROLE",
+ "principalId": "$owner",
+ "permission": "ALLOW",
+ "property": "replaceById"
+ },
{
"principalType": "ROLE",
"principalId": "$everyone",
diff --git a/docs.json b/docs.json
index c99680b27..c951ec91f 100644
--- a/docs.json
+++ b/docs.json
@@ -5,7 +5,7 @@
"lib/server-app.js",
"lib/loopback.js",
"lib/registry.js",
- "server/current-context.js",
+ "lib/current-context.js",
"lib/access-context.js",
{ "title": "Base models", "depth": 2 },
"lib/model.js",
@@ -24,6 +24,7 @@
"common/models/application.js",
"common/models/change.js",
"common/models/email.js",
+ "common/models/key-value-model.js",
"common/models/role.js",
"common/models/role-mapping.js",
"common/models/scope.js",
diff --git a/example/client-server/client.js b/example/client-server/client.js
index 436e266b8..63cd4e2e9 100644
--- a/example/client-server/client.js
+++ b/example/client-server/client.js
@@ -1,3 +1,10 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
+
var loopback = require('../../');
var client = loopback();
var CartItem = require('./models').CartItem;
@@ -11,10 +18,10 @@ CartItem.attachTo(remote);
// call the remote method
CartItem.sum(1, function(err, total) {
- console.log('result:', err || total);
+ g.log('result:%s', err || total);
});
// call a built in remote method
CartItem.find(function(err, items) {
- console.log(items);
+ g.log(items);
});
diff --git a/example/client-server/models.js b/example/client-server/models.js
index 34d5c8bac..ac1b14320 100644
--- a/example/client-server/models.js
+++ b/example/client-server/models.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../../');
var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', {
diff --git a/example/client-server/server.js b/example/client-server/server.js
index 7e466a563..52c738d3a 100644
--- a/example/client-server/server.js
+++ b/example/client-server/server.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../../');
var server = module.exports = loopback();
var CartItem = require('./models').CartItem;
diff --git a/example/colors/app.js b/example/colors/app.js
index e182f926b..44d75c493 100644
--- a/example/colors/app.js
+++ b/example/colors/app.js
@@ -1,3 +1,10 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
+
var loopback = require('../../');
var app = loopback();
@@ -7,9 +14,9 @@ var schema = {
name: String
};
-var Color = app.model('color', schema);
-
-app.dataSource('db', {adapter: 'memory'}).attach(Color);
+app.dataSource('db', { connector: 'memory' });
+var Color = app.registry.createModel('color', schema);
+app.model(Color, { dataSource: 'db' });
Color.create({name: 'red'});
Color.create({name: 'green'});
@@ -17,4 +24,4 @@ Color.create({name: 'blue'});
app.listen(3000);
-console.log('a list of colors is available at http://localhost:3000/colors');
+g.log('a list of colors is available at {{http://localhost:3000/colors}}');
diff --git a/example/context/app.js b/example/context/app.js
index 12cedc078..8cf43e7e7 100644
--- a/example/context/app.js
+++ b/example/context/app.js
@@ -1,3 +1,10 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
+
var loopback = require('../../');
var app = loopback();
@@ -17,7 +24,7 @@ var Color = loopback.createModel('color', { 'name': String });
Color.beforeRemote('**', function (ctx, unused, next) {
// Inside LoopBack code, you can read the property from the context
var ns = loopback.getCurrentContext();
- console.log('Request to host', ns && ns.get('host'));
+ g.log('Request to host %s', ns && ns.get('host'));
next();
});
@@ -25,5 +32,5 @@ app.dataSource('db', { connector: 'memory' });
app.model(Color, { dataSource: 'db' });
app.listen(3000, function() {
- console.log('A list of colors is available at http://localhost:3000/colors');
+ g.log('A list of colors is available at {{http://localhost:3000/colors}}');
});
diff --git a/example/mobile-models/app.js b/example/mobile-models/app.js
index e7e3c582b..fe5f4765d 100644
--- a/example/mobile-models/app.js
+++ b/example/mobile-models/app.js
@@ -1,3 +1,10 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
+
var models = require('../../lib/models');
var loopback = require('../../');
@@ -32,14 +39,15 @@ var data = {pushSettings: [
]}
Application.create(data, function(err, data) {
- console.log('Created: ', data.toObject());
+ g.log('Created: %s', data.toObject());
});
-Application.register('rfeng', 'MyApp', {description: 'My first mobile application'}, function (err, result) {
- console.log(result.toObject());
+Application.register('rfeng', 'MyApp', { description: g.f('My first mobile application') },
+ function(err, result) {
+ console.log(result.toObject());
- result.resetKeys(function (err, result) {
- console.log(result.toObject());
- });
+ result.resetKeys(function(err, result) {
+ console.log(result.toObject());
+ });
});
diff --git a/example/replication/app.js b/example/replication/app.js
index ab6e69870..5ab3741d8 100644
--- a/example/replication/app.js
+++ b/example/replication/app.js
@@ -1,8 +1,15 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../../');
var app = loopback();
-var db = app.dataSource('db', {connector: loopback.Memory});
-var Color = app.model('color', {dataSource: 'db', options: {trackChanges: true}});
-var Color2 = app.model('color2', {dataSource: 'db', options: {trackChanges: true}});
+var db = app.dataSource('db', { connector: 'memory' });
+var Color = app.registry.createModel('color', {}, { trackChanges: true });
+app.model(Color, { dataSource: 'db' });
+var Color2 = app.registry.createModel('color2', {}, { trackChanges: true });
+app.model(Color2, { dataSource: 'db' });
var target = Color2;
var source = Color;
var SPEED = process.env.SPEED || 100;
diff --git a/example/simple-data-source/app.js b/example/simple-data-source/app.js
index 3964df77b..3074d7b9e 100644
--- a/example/simple-data-source/app.js
+++ b/example/simple-data-source/app.js
@@ -1,3 +1,10 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
+
var loopback = require('../../');
var app = loopback();
@@ -20,4 +27,4 @@ Color.all(function () {
app.listen(3000);
-console.log('a list of colors is available at http://localhost:3000/colors');
+g.log('a list of colors is available at {{http://localhost:3000/colors}}');
diff --git a/index.js b/index.js
index 1512239a7..0f02a1c1e 100644
--- a/index.js
+++ b/index.js
@@ -1,3 +1,11 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var SG = require('strong-globalize');
+SG.SetRootDir(__dirname, { autonomousMsgLoading: 'all' });
+
/**
* loopback ~ public api
*/
diff --git a/intl/de/messages.json b/intl/de/messages.json
new file mode 100644
index 000000000..31d7d99c3
--- /dev/null
+++ b/intl/de/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "Kein Änderungssatz gefunden für {0} mit ID {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "Für die Authentifizierung muss Modell {0} definiert sein.",
+ "0731d0109e46c21a4e34af3346ed4856": "Dieses Verhalten kann sich in der nächsten Hauptversion ändern.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Zugriff verweigert",
+ "0caffe1d763c8cca6a61814abe33b776": "E-Mail ist erforderlich",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "Der Antworthauptteil enthält Eigenschaften des bei der Anmeldung erstellten {{AccessToken}}.\nAbhängig vom Wert des Parameters 'include' kann der Hauptteil zusätzliche Eigenschaften enthalten:\n\n - user - U+007BUserU+007D - Daten des derzeit angemeldeten Benutzers. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t BETREFF:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Erstellt: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t VON:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "{0} mit ID {1} konnte nicht gefunden werden",
+ "2d78192c43fd2ec52ec18f3918894f9a": "Middleware {0} ist veraltet. Siehe {1} für weitere Details.",
+ "308e1d484516a33df788f873e65faaff": "Modell '{0}' bietet veraltetes 'DataModel' an. Verwenden Sie stattdessen 'PersistedModel'.",
+ "316e5b82c203cf3de31a449ee07d0650": "Erwartet wurde boolescher Wert, {0} empfangen",
+ "320c482401afa1207c04343ab162e803": "Ungültiger Prinzipaltyp: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "Die relations-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Bestätigen Sie Ihre E-Mail-Adresse, indem Sie diesen Link in einem Web-Browser öffnen:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "Anmeldung fehlgeschlagen, da die E-Mail-Adresse nicht bestätigt wurde",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Unbekannte {{middleware}}-Phase {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "Ungültiges Token: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Vielen Dank für die Registrierung",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Warnung: Keine E-Mail-Transportmethode für das Senden von E-Mails angegeben. Richten Sie eine Transportmethode für das Senden von E-Mails ein.",
+ "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{PersistedModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "E-Mail nicht gefunden",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "E-Mail senden:",
+ "4b494de07f524703ac0879addbd64b13": "E-Mail-Adresse wurde nicht bestätigt",
+ "4cac5f051ae431321673e04045d37772": "Modell '{0}' bietet das unbekannte Modell '{1}' an. 'PersistedModel' wird als Basis verwendet.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Kann Datenquelle {0} nicht erstellen: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "Sie müssen das {{Email}}-Modell mit einem {{Mail}}-Konnektor verbinden",
+ "5e81ad3847a290dc650b47618b9cbc7e": "Anmeldung fehlgeschlagen",
+ "5fa3afb425819ebde958043e598cb664": "Modell mit {{id}} {0} konnte nicht gefunden werden",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "Beziehung '{0} ist für Modell {1} nicht vorhanden",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl aktualisierter Datensätze nicht richtig.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Fehlende Daten für Änderung: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} konnte nicht gefunden werden",
+ "734a7bebb65e10899935126ba63dd51f": "Die options-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein",
+ "779467f467862836e19f494a37d6ab77": "Die acls-Eigenschaft der Konfiguration '{0}' muss eine Reihe von Objekten sein",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "Meine erste mobile Anwendung",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Ungültiges Zugriffstoken",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Berechtigung erforderlich",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} muss sich abmelden",
+ "80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschaft '{0}' kann für {1} nicht rekonfiguriert werden",
+ "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" unbekannt, ID \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "Die Transportmethode unterstützt keine HTTP-Umleitungen.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} oder {{email}} ist erforderlich",
+ "8a17c5ef611e2e7535792316e66b8fca": "Kennwort zu lang: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Anforderung an Host {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Ungültige Remote-Methode: '{0}'",
+ "8bab6720ecc58ec6412358c858a53484": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen geändert: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "Der Konfiguration von {0} fehlt die {{`dataSource`}}-Eigenschaft.\nVerwenden Sie 'null' oder 'false', um Modelle zu kennzeichnen, die mit keiner Datenquelle verbunden sind.",
+ "a40684f5a9f546115258b76938d1de37": "Eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "Benutzer nicht gefunden: {0}",
+ "a80038252430df2754884bf3c845c4cf": "Den Remote-Anbindungs-Metadaten für \"{0}.{1}\" fehlt das Flag \"isStatic\"; die Methode ist als Instanzdefinitionsmethode registriert.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" unbekannt, {{key}} \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} ist erforderlich",
+ "c2b5d51f007178170ca3952d59640ca4": "{0} Änderungen können nicht behoben werden:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "Eine gültige E-Mail-Adresse muss angegeben werden",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "muss {{id}} oder {{data}} angeben",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} wurde entfernt, verwenden Sie stattdessen das neue Modul {{loopback-boot}}",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} ist veraltet. Siehe {1} für weitere Details.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "Nicht-Objekt-Einstellung \"{0}\" von \"methods\" wird ignoriert.",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" unbekannt, {{id}} \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen gelöscht : {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl gelöschter Datensätze nicht richtig.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{KeyValueModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t AN:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTMETHODE:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "Ergebnis:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Konflikt",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Ungültiges Kennwort: {0}"
+}
+
diff --git a/intl/en/messages.json b/intl/en/messages.json
new file mode 100644
index 000000000..4e6decd4b
--- /dev/null
+++ b/intl/en/messages.json
@@ -0,0 +1,75 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "No change record found for {0} with id {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "Authentication requires model {0} to be defined.",
+ "0731d0109e46c21a4e34af3346ed4856": "This behaviour may change in the next major version.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Access Denied",
+ "0caffe1d763c8cca6a61814abe33b776": "Email is required",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "a list of colors is available at {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t SUBJECT:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Created: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t FROM:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "could not find {0} with id {1}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0} middleware is deprecated. See {1} for more details.",
+ "308e1d484516a33df788f873e65faaff": "Model `{0}` is extending deprecated `DataModel. Use `PersistedModel` instead.",
+ "316e5b82c203cf3de31a449ee07d0650": "Expected boolean, got {0}",
+ "320c482401afa1207c04343ab162e803": "Invalid principal type: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "The relations property of `{0}` configuration must be an object",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Please verify your email by opening this link in a web browser:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "login failed as the email has not been verified",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Unknown {{middleware}} phase {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "Invalid token: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Thanks for Registering",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Warning: No email transport specified for sending email. Setup a transport to send mail messages.",
+ "4203ab415ec66a78d3164345439ba76e": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "Email not found",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Sending Mail:",
+ "4b494de07f524703ac0879addbd64b13": "Email has not been verified",
+ "4cac5f051ae431321673e04045d37772": "Model `{0}` is extending an unknown model `{1}`. Using `PersistedModel` as the base.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Cannot create data source {0}: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "You must connect the {{Email}} Model to a {{Mail}} connector",
+ "5e81ad3847a290dc650b47618b9cbc7e": "login failed",
+ "5fa3afb425819ebde958043e598cb664": "could not find a model with {{id}} {0}",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "Relation `{0}` does not exist for model `{1}`",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Cannot apply bulk updates, the connector does not correctly report the number of updated records.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Missing data for change: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "Could not find {{accessToken}}",
+ "734a7bebb65e10899935126ba63dd51f": "The options property of `{0}` configuration must be an object",
+ "779467f467862836e19f494a37d6ab77": "The acls property of `{0}` configuration must be an array of objects",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "My first mobile application",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Invalid Access Token",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Authorization Required",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is required to logout",
+ "80a32e80cbed65eba2103201a7c94710": "Model not found: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "Property `{0}` cannot be reconfigured for `{1}`",
+ "855ecd4a64885ba272d782435f72a4d4": "Unknown \"{0}\" id \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "The transport does not support HTTP redirects.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} or {{email}} is required",
+ "8a17c5ef611e2e7535792316e66b8fca": "Password too long: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Request to host {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Invalid remote method: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "Bulk update failed, the connector has modified unexpected number of records: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "The configuration of `{0}` is missing {{`dataSource`}} property.\nUse `null` or `false` to mark models not attached to any data source.",
+ "a40684f5a9f546115258b76938d1de37": "A list of colors is available at {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "User not found: {0}",
+ "a80038252430df2754884bf3c845c4cf": "Remoting metadata for \"{0}.{1}\" is missing \"isStatic\" flag, the method is registered as an instance method.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "Unknown \"{0}\" {{key}} \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is required",
+ "c2b5d51f007178170ca3952d59640ca4": "Cannot rectify {0} changes:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "Must provide a valid email",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "must specify an {{id}} or {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} is deprecated. See {1} for more details.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "Ignoring non-object \"methods\" setting of \"{0}\".",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Unknown \"{0}\" {{id}} \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "Bulk update failed, the connector has deleted unexpected number of records: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Cannot apply bulk updates, the connector does not correctly report the number of deleted records.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{KeyValueModel}} has not been correctly attached to a {{DataSource}}!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t TO:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "result:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Conflict",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Invalid password: {0}"
+}
diff --git a/intl/es/messages.json b/intl/es/messages.json
new file mode 100644
index 000000000..7abc0eca4
--- /dev/null
+++ b/intl/es/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "No se ha encontrado ningún registro de cambio para {0} con el id {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "La autenticación requiere la definición del modelo {0}.",
+ "0731d0109e46c21a4e34af3346ed4856": "Este comportamiento puede cambiar en la próxima versión principal.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado",
+ "0caffe1d763c8cca6a61814abe33b776": "Es necesario el correo electrónico",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "una lista de colores está disponible en {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión.\nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t ASUNTO:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Creado: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t DESDE:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "no se ha encontrado {0} con el ID {1}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "El middleware {0} está en desuso. Consulte {1} para obtener detalles.",
+ "308e1d484516a33df788f873e65faaff": "El modelo `{0}` está ampliando `DataModel` en desuso. Utilice `PersistedModel` en su lugar.",
+ "316e5b82c203cf3de31a449ee07d0650": "Se esperaba un booleano, se ha obtenido {0}",
+ "320c482401afa1207c04343ab162e803": "Tipo de principal no válido: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "La configuración de la propiedad relations de `{0}` debe ser un objeto",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Verifique su correo electrónico abriendo este enlace en un navegador:\n\t {0}",
+ "3aae63bb7e8e046641767571c1591441": "el inicio de sesión ha fallado porque el correo electrónico no ha sido verificado",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Fase de {{middleware}} desconocida {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Gracias por registrarse",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Aviso: No se ha especificado ningún transporte de correo electrónico para enviar correo electrónico. Configure un transporte para enviar mensajes de correo.",
+ "4203ab415ec66a78d3164345439ba76e": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{PersistedModel}} no se ha conectado correctamente a un {{DataSource}}.",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "Correo electrónico no encontrado",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando correo:",
+ "4b494de07f524703ac0879addbd64b13": "El correo electrónico no se ha verificado",
+ "4cac5f051ae431321673e04045d37772": "El modelo `{0}` está ampliando un modelo desconocido `{1}`. Se utiliza `PersistedModel` como base.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "No se puede crear el origen de datos {0}: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "Debe conectar el modelo de {{Email}} a un conector de {{Mail}}",
+ "5e81ad3847a290dc650b47618b9cbc7e": "el inicio de sesión ha fallado",
+ "5fa3afb425819ebde958043e598cb664": "no se ha encontrado un modelo con {{id}} {0}",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "La relación `{0}` no existe para el modelo `{1}`",
+ "62e8b0a733417978bab22c8dacf5d7e6": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros actualizados.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Faltan datos para el cambio: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "No se ha encontrado {{accessToken}}",
+ "734a7bebb65e10899935126ba63dd51f": "La configuración de la propiedad de options de `{0}` debe ser un objeto",
+ "779467f467862836e19f494a37d6ab77": "La configuración de la propiedad acls de `{0}` debe ser una matriz de objetos",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "Mi primera aplicación móvil",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Autorización necesaria",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "Es necesario {{accessToken}} para cerrar la sesión",
+ "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "La propiedad `{0}` no puede reconfigurarse para `{1}`",
+ "855ecd4a64885ba272d782435f72a4d4": "Id de \"{0}\" desconocido \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "El transporte no admite redirecciones HTTP.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} o {{email}} es obligatorio",
+ "8a17c5ef611e2e7535792316e66b8fca": "Contraseña demasiado larga: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitud al host {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Método remoto no válido: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "La actualización masiva ha fallado, el conector ha modificado un número de registros inesperado: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "En la configuración de `{0}` falta la propiedad {{`dataSource`}}.\nUtilice `null` o `false` para marcar los modelos no conectados a ningún origen de datos.",
+ "a40684f5a9f546115258b76938d1de37": "Una lista de colores está disponible en {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "No se ha encontrado el usuario: {0}",
+ "a80038252430df2754884bf3c845c4cf": "En los metadatos de interacción remota para \"{0}.{1}\" falta el indicador \"isStatic\", el método está registrado como método de instancia.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "{{key}} de \"{0}\" desconocido \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} es obligatorio",
+ "c2b5d51f007178170ca3952d59640ca4": "No se pueden rectificar los cambios de {0}:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "Debe proporcionar un correo electrónico válido",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "debe especificar un {{id}} o {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} se ha eliminado, utilice el nuevo módulo {{loopback-boot}} en su lugar",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} está en desuso. Consulte {1} para obtener detalles.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "Se ignora el valor \"methods\" no de objeto de \"{0}\".",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} de \"{0}\" desconocido \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "La actualización masiva ha fallado, el conector ha suprimido un número de registros inesperado: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros suprimidos.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{KeyValueModel}} no se ha conectado correctamente a un {{DataSource}}.",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Conflicto",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Contraseña no válida: {0}"
+}
+
diff --git a/intl/fr/messages.json b/intl/fr/messages.json
new file mode 100644
index 000000000..cd5537d46
--- /dev/null
+++ b/intl/fr/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "Aucun enregistrement de changement trouvé pour {0} avec l'id {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "L'authentification exige que le modèle {0} soit défini.",
+ "0731d0109e46c21a4e34af3346ed4856": "Ce comportement peut changer dans la version principale suivante.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Accès refusé",
+ "0caffe1d763c8cca6a61814abe33b776": "L'adresse électronique est obligatoire",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "une liste de couleurs est disponible sur {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "Le corps de réponse contient les propriétés de {{AccessToken}} créées lors de la connexion.\nEn fonction de la valeur du paramètre `include`, le corps peut contenir des propriétés supplémentaires :\n\n - `user` - `U+007BUserU+007D` - Données de l'utilisateur connecté. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t SUJET :{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Création de : {0}",
+ "275f22ab95671f095640ca99194b7635": "\t DE :{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "impossible de trouver {0} avec l'id {1}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "Le middleware {0} est obsolète. Pour plus de détails, voir {1}.",
+ "308e1d484516a33df788f873e65faaff": "Le modèle `{0}` étend le `DataModel obsolète. Utilisez à la place `PersistedModel`.",
+ "316e5b82c203cf3de31a449ee07d0650": "Valeur booléenne attendue, {0} obtenu",
+ "320c482401afa1207c04343ab162e803": "Type de principal non valide : {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "La propriété relations de la configuration `{0}` doit être un objet",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Vérifiez votre courrier électronique en ouvrant ce lien dans un navigateur Web :\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "la connexion a échoué car l'adresse électronique n'a pas été vérifiée",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Phase {{middleware}} inconnue {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "Jeton non valide : {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Merci pour votre inscription",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Avertissement : Aucun transport de courrier électronique n'est spécifié pour l'envoi d'un message électronique. Configurez un transport pour envoyer des messages électroniques.",
+ "4203ab415ec66a78d3164345439ba76e": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{PersistedModel}} n'a pas été associé correctement à {{DataSource}} !",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "Adresse électronique introuvable",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Envoi d'un message électronique :",
+ "4b494de07f524703ac0879addbd64b13": "Le courrier électronique n'a pas été vérifié",
+ "4cac5f051ae431321673e04045d37772": "Le modèle `{0}` étend un modèle inconnu `{1}`. Utilisation de `PersistedModel` comme base.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossible de créer la source de données {0} : {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "Vous devez connecter le modèle {{Email}} à un connecteur {{Mail}}",
+ "5e81ad3847a290dc650b47618b9cbc7e": "échec de la connexion",
+ "5fa3afb425819ebde958043e598cb664": "impossible de trouver un modèle avec {{id}} {0}",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "La relation `{0}` n'existe pas pour le modèle `{1}`",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements mis à jour.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TEXTE :{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Données manquantes pour le changement : {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} introuvable",
+ "734a7bebb65e10899935126ba63dd51f": "La propriété options de la configuration `{0}` doit être un objet",
+ "779467f467862836e19f494a37d6ab77": "La propriété acls de la configuration `{0}` doit être un tableau d'objets",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "Ma première application mobile",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Jeton d'accès non valide",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Autorisation requise",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} est nécessaire pour la déconnexion",
+ "80a32e80cbed65eba2103201a7c94710": "Modèle introuvable : {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "La propriété `{0}` ne peut pas être reconfigurée pour `{1}`",
+ "855ecd4a64885ba272d782435f72a4d4": "ID \"{0}\" inconnu \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "Le transport ne prend pas en charge les réacheminements HTTP.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} est obligatoire",
+ "8a17c5ef611e2e7535792316e66b8fca": "Mot de passe trop long : {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Demande à l'hôte {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Méthode distante non valide : `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "La mise à jour en bloc a échoué ; le connecteur a modifié un nombre inattendu d'enregistrements : {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML :{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "La propriété {{`dataSource`}} est manquante dans la configuration de `{0}`.\nUtilisez `null` ou `false` pour marquer les modèles non associés à une source de données.",
+ "a40684f5a9f546115258b76938d1de37": "Une liste de couleurs est disponible sur {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "Utilisateur introuvable : {0}",
+ "a80038252430df2754884bf3c845c4cf": "Métadonnées remoting pour \"{0}.{1}\" ne comporte pas l'indicateur \"isStatic\" ; la méthode est enregistrée en tant que méthode instance.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" inconnu.",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} est obligatoire",
+ "c2b5d51f007178170ca3952d59640ca4": "Impossible de rectifier les modifications {0} :\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "Obligation de fournir une adresse électronique valide",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "obligation de spécifier {{id}} ou {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} a été supprimé ; utilisez à la place le nouveau module {{loopback-boot}}",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} est obsolète. Pour plus de détails, voir {1}.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "Le paramètre \"methods\" non objet de \"{0}\" est ignoré.",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" inconnu.",
+ "e92aa25b6b864e3454b65a7c422bd114": "La mise à jour en bloc a échoué ; le connecteur a supprimé un nombre inattendu d'enregistrements : {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements supprimés.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{KeyValueModel}} n'a pas été associé correctement à {{DataSource}} !",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t A :{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT :{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "résultat :{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Conflit",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Mot de passe non valide : {0}"
+}
+
diff --git a/intl/it/messages.json b/intl/it/messages.json
new file mode 100644
index 000000000..f4b23f5cc
--- /dev/null
+++ b/intl/it/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "Nessun record di modifica trovato per {0} con id {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "L'autenticazione richiede che sia definito il modello {0}.",
+ "0731d0109e46c21a4e34af3346ed4856": "Questo funzionamento può essere modificato nella versione principale successiva.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Accesso negato",
+ "0caffe1d763c8cca6a61814abe33b776": "L'email è obbligatoria",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso.\nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t OGGETTO:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Creato: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t DA:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "impossibile trovare {0} con id {1}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0} middleware is deprecated. Consultare {1} per ulteriori dettagli.",
+ "308e1d484516a33df788f873e65faaff": "Il modello `{0}` estende il modello `DataModel obsoleto. Utilizzare `PersistedModel`.",
+ "316e5b82c203cf3de31a449ee07d0650": "Previsto valore booleano, ricevuto {0}",
+ "320c482401afa1207c04343ab162e803": "Tipo principal non valido: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "La proprietà relations della configurazione `{0}` deve essere un oggetto",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Verificare la e-mail aprendo questo link in un browser web:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "login non riuscito perché l'email non è stata verificata",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {{middleware}} sconosciuta {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "Token non valido: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Grazie per essersi registrati",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Avvertenza: nessun trasporto email specificato per l'invio della email. Configurare un trasporto per inviare messaggi email.",
+ "4203ab415ec66a78d3164345439ba76e": "Impossibile richiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{PersistedModel}} non è stato correttamente collegato ad una {{DataSource}}!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "Email non trovata",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Invio email:",
+ "4b494de07f524703ac0879addbd64b13": "La e-mail non è stata verificata",
+ "4cac5f051ae431321673e04045d37772": "Il modello `{0}` estende un modello sconosciuto `{1}`. Viene utilizzato `PersistedModel` come base.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossibile creare l'origine dati {0}: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "È necessario collegare il modello {{Email}} ad un connettore {{Mail}}",
+ "5e81ad3847a290dc650b47618b9cbc7e": "login non riuscito",
+ "5fa3afb425819ebde958043e598cb664": "impossibile trovare un modello con {{id}} {0}",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "La relazione `{0}` non esiste per il modello `{1}`",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record aggiornati.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TESTO:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Dati mancanti per la modifica: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "Could not find {{accessToken}}",
+ "734a7bebb65e10899935126ba63dd51f": "La proprietà options della configurazione `{0}` deve essere un oggetto",
+ "779467f467862836e19f494a37d6ab77": "La proprietà acls della configurazione `{0}` deve essere un array di oggetti",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "Prima applicazione mobile personale",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Token di accesso non valido",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Autorizzazione richiesta",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is required to logout",
+ "80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "Impossibile riconfigurare la proprietà `{0}` per `{1}`",
+ "855ecd4a64885ba272d782435f72a4d4": "ID sconosciuto \"{0}\" \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "Il trasporto non supporta i reindirizzamenti HTTP.",
+ "895b1f941d026870b3cc8e6af087c197": "Sono richiesti {{username}} o {{email}}",
+ "8a17c5ef611e2e7535792316e66b8fca": "Password troppo lunga: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Richiesta all'host {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Metodo remoto non valido: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "Aggiornamento in massa non riuscito, il connettore ha modificato un numero non previsto di record: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "La configurazione di `{0}` non contiene la proprietà {{`dataSource`}}.\nUtilizzare `null` o `false` per contrassegnare i modelli non collegati ad alcuna origine dati.",
+ "a40684f5a9f546115258b76938d1de37": "Un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "Utente non trovato: {0}",
+ "a80038252430df2754884bf3c845c4cf": "Metadati della comunicazione in remoto per \"{0}.{1}\" non presenta l'indicatore \"isStatic\", il metodo è registrato come metodo dell'istanza.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "{{key}} \"{0}\" sconosciuto \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} è obbligatorio",
+ "c2b5d51f007178170ca3952d59640ca4": "Impossibile correggere {0} modifiche:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "È necessario fornire una email valida",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "è necessario specificare {{id}} o {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} è stato rimosso, utilizzare il nuovo modulo {{loopback-boot}}",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} is deprecated. Consultare {1} per ulteriori dettagli.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "L'impostazione \"methods\" non oggetto di \"{0}\" viene ignorata.",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} \"{0}\" sconosciuto \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "Aggiornamento in massa non riuscito, il connettore ha eliminato un numero non previsto di record: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record eliminati.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "Impossibile richiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{KeyValueModel}} non è stato correttamente collegato ad una {{DataSource}}!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRASPORTO:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "risultato:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Conflitto",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Password non valida: {0}"
+}
+
diff --git a/intl/ja/messages.json b/intl/ja/messages.json
new file mode 100644
index 000000000..7f793f7be
--- /dev/null
+++ b/intl/ja/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "ID {1} の {0} の変更レコードが見つかりませんでした",
+ "04bd8af876f001ceaf443aad6a9002f9": "認証では、モデル {0} を定義する必要があります。",
+ "0731d0109e46c21a4e34af3346ed4856": "この動作は次のメジャー・バージョンで変更される可能性があります。",
+ "095afbf2f1f0e5be678f5dac5c54e717": "アクセス拒否",
+ "0caffe1d763c8cca6a61814abe33b776": "E メールは必須です",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "カラー・リストは {{http://localhost:3000/colors}} で利用できます",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "応答本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n`include` パラメーターの値によっては、本文に追加のプロパティーが含まれる場合があります:\n\n - `user` - `U+007BUserU+007D` - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "作成済み: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t 送信元:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "ID {1} の {0} が見つかりませんでした",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0} ミドルウェアは非推奨です。詳しくは、{1} を参照してください。",
+ "308e1d484516a33df788f873e65faaff": "モデル `{0}` は非推奨の `DataModel を拡張しています。 代わりに `PersistedModel` を使用してください。",
+ "316e5b82c203cf3de31a449ee07d0650": "ブール値が必要ですが、{0} が取得されました",
+ "320c482401afa1207c04343ab162e803": "無効なプリンシパル・タイプ: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の関係プロパティーはオブジェクトでなければなりません",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Web ブラウザーで次のリンクを開いて、E メールを検証してください: \n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "E メールが検証されていないため、ログインに失敗しました",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} フェーズ {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "ご登録いただき、ありがとうございます。",
+ "3d63008ccfb2af1db2142e8cc2716ace": "警告: E メール送信用の E メール・トランスポートが指定されていません。 メール・メッセージを送信するためのトランスポートをセットアップしてください。",
+ "4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{PersistedModel}} は {{DataSource}} に正しく付加されていません。",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "E メールが見つかりません",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "メールの送信:",
+ "4b494de07f524703ac0879addbd64b13": "E メールが検証されていません",
+ "4cac5f051ae431321673e04045d37772": "モデル `{0}` は不明のモデル `{1}` を拡張しています。 ベースとして `PersistedModel` を使用します。",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0}: {1} を作成できません",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります",
+ "5e81ad3847a290dc650b47618b9cbc7e": "ログインに失敗しました",
+ "5fa3afb425819ebde958043e598cb664": "{{id}} {0} のモデルが見つかりませんでした",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "モデル `{1}` には関係 `{0}` が存在しません",
+ "62e8b0a733417978bab22c8dacf5d7e6": "一括更新を適用できません。コネクターは更新されたレコードの数を正しく報告していません。",
+ "63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "変更用のデータがありません: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} が見つかりませんでした",
+ "734a7bebb65e10899935126ba63dd51f": "`{0}` 構成のオプション・プロパティーはオブジェクトでなければなりません",
+ "779467f467862836e19f494a37d6ab77": "`{0}` 構成の ACL プロパティーはオブジェクトの配列でなければなりません",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "最初のモバイル・アプリケーション",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "無効なアクセス・トークン",
+ "7e287fc885d9fdcf42da3a12f38572c1": "許可が必要です",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "ログアウトするには {{accessToken}} が必要です",
+ "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}` のプロパティー `{0}` を再構成できません",
+ "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" が不明です。",
+ "860d1a0b8bd340411fb32baa72867989": "トランスポートでは HTTP リダイレクトはサポートされません。",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} または {{email}} が必要です",
+ "8a17c5ef611e2e7535792316e66b8fca": "パスワードが長すぎます: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "ホスト {0} への要求",
+ "8ae418c605b6a45f2651be9b1677c180": "無効なリモート・メソッド: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "一括更新が失敗しました。コネクターは予期しない数のレコードを変更しました: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` の構成は {{`dataSource`}} プロパティーがありません。\nどのデータ・ソースにも付加されていないモデルにマークを付けるには `null` または `false` を使用します。",
+ "a40684f5a9f546115258b76938d1de37": "カラー・リストは {{http://localhost:3000/colors}} で利用できます",
+ "a50d10fc6e0959b220e085454c40381e": "ユーザーが見つかりません: {0}",
+ "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" のリモート・メタデータに「isStatic」フラグがありません。このメソッドはインスタンス・メソッドとして登録されます。",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" が不明です。",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} は必須です",
+ "c2b5d51f007178170ca3952d59640ca4": "{0} の変更を修正できません:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "有効な E メールを指定する必要があります",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} または {{data}} を指定する必要があります",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} は非推奨です。詳しくは、{1} を参照してください。",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" の非オブジェクト「メソッド」設定を無視します。",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" が不明です。",
+ "e92aa25b6b864e3454b65a7c422bd114": "一括更新が失敗しました。コネクターは予期しない数のレコードを削除しました: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。コネクターは削除されたレコードの数を正しく報告していません。",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{KeyValueModel}} は {{DataSource}} に正しく付加されていません。",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t 宛先:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t トランスポート:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "競合",
+ "f58cdc481540cd1f69a4aa4da2e37981": "無効なパスワード: {0}"
+}
+
diff --git a/intl/ko/messages.json b/intl/ko/messages.json
new file mode 100644
index 000000000..b027adee0
--- /dev/null
+++ b/intl/ko/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "ID가 {1}인 {0}에 대한 변경 레코드를 찾을 수 없음",
+ "04bd8af876f001ceaf443aad6a9002f9": "인증을 위해 {0} 모델이 정의되어야 함",
+ "0731d0109e46c21a4e34af3346ed4856": "이 동작은 다음 주요 버전에서 변경될 수 있습니다.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "액세스 거부",
+ "0caffe1d763c8cca6a61814abe33b776": "이메일은 필수입니다.",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "색상 목록은 {{http://localhost:3000/colors}}에 있음",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "응답 본문에 로그인 시 작성한 {{AccessToken}} 특성이 포함됩니다. \n`include` 매개변수 값에 따라 본문에 추가 특성이 포함될 수 있습니다. \n\n - `user` - `U+007BUserU+007D` - 현재 로그인된 사용자의 데이터. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t 제목:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "작성 날짜: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t 발신인:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "ID {1}(으)로 {0}을(를) 찾을 수 없음 ",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0} 미들웨어는 더 이상 사용되지 않습니다. 자세한 정보는 {1}을(를) 참조하십시오. ",
+ "308e1d484516a33df788f873e65faaff": "모델 `{0}`은(는) 더 이상 사용되지 않는 `DataModel`의 확장입니다. 대신 `PersistedModel`을 사용하십시오.",
+ "316e5b82c203cf3de31a449ee07d0650": "예상 부울, 실제 {0}",
+ "320c482401afa1207c04343ab162e803": "올바르지 않은 프린시펄 유형: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 구성의 관계 특성은 오브젝트여야 함",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "웹 브라우저에서 이 링크를 열어 이메일을 확인하십시오.\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "이메일이 확인되지 않아서 로그인에 실패했습니다. ",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "알 수 없는 {{middleware}} 단계 {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "올바르지 않은 토큰: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "등록해 주셔서 감사합니다.",
+ "3d63008ccfb2af1db2142e8cc2716ace": "경고: 이메일 발송을 위해 이메일 전송이 지정되지 않았습니다. 메일 메시지를 보내려면 전송을 설정하십시오. ",
+ "4203ab415ec66a78d3164345439ba76e": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{PersistedModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "이메일을 찾을 수 없음",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "메일 발송 중:",
+ "4b494de07f524703ac0879addbd64b13": "이메일이 확인되지 않았습니다.",
+ "4cac5f051ae431321673e04045d37772": "모델 `{0}`은(는) 알 수 없는 모델 `{1}`의 확장입니다. `PersistedModel`을 기본으로 사용하십시오.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "데이터 소스 {0}을(를) 작성할 수 없음: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} 모델을 {{Mail}} 커넥터에 연결해야 합니다. ",
+ "5e81ad3847a290dc650b47618b9cbc7e": "로그인 실패",
+ "5fa3afb425819ebde958043e598cb664": "{{id}} {0}인 모델을 찾을 수 없음",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "모델 `{1}`에 대해 관계 `{0}`이(가) 없습니다. ",
+ "62e8b0a733417978bab22c8dacf5d7e6": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 업데이트된 레코드 수를 제대로 보고하지 않습니다. ",
+ "63a091ced88001ab6acb58f61ec041c5": "\t 텍스트:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "변경을 위한 데이터 누락: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}}을(를) 찾을 수 없음",
+ "734a7bebb65e10899935126ba63dd51f": "`{0}` 구성의 옵션 특성은 오브젝트여야 함",
+ "779467f467862836e19f494a37d6ab77": "`{0}` 구성의 acls 특성은 오브젝트 배열이어야 함",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "내 첫 번째 모바일 애플리케이션",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "올바르지 않은 액세스 토큰",
+ "7e287fc885d9fdcf42da3a12f38572c1": "권한 필수",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}}이(가) 로그아웃해야 함",
+ "80a32e80cbed65eba2103201a7c94710": "모델을 찾을 수 없음: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}`에 대해 `{0}` 특성을 다시 구성할 수 없음",
+ "855ecd4a64885ba272d782435f72a4d4": "알 수 없는 \"{0}\" ID \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "전송에서 HTTP 경로 재지원을 지원하지 않습니다.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} 또는 {{email}}은(는) 필수입니다.",
+ "8a17c5ef611e2e7535792316e66b8fca": "비밀번호가 너무 김: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "호스트 {0}에 요청",
+ "8ae418c605b6a45f2651be9b1677c180": "올바르지 않은 원격 메소드: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 수정했습니다. {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}`의 구성에 {{`dataSource`}} 특성이 누락되었습니다.\n데이터 소스에 첨부되지 않은 모델을 표시하려면 `null` 또는 `false`를 사용하십시오.",
+ "a40684f5a9f546115258b76938d1de37": "색상 목록은 {{http://localhost:3000/colors}}에 있음",
+ "a50d10fc6e0959b220e085454c40381e": "사용자를 찾을 수 없음: {0}",
+ "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\"에 대한 원격 메타데이터에 \"isStatic\" 플래그가 누락되었습니다. 이 메소드는 인스턴스 메소드로 등록되어 있습니다.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "알 수 없는 \"{0}\" {{key}} \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}}은(는) 필수입니다.",
+ "c2b5d51f007178170ca3952d59640ca4": "{0} 변경사항을 교정할 수 없음:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "올바른 이메일을 제공해야 함",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} 또는 {{data}}을(를) 지정해야 함",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}}이(가) 제거되었습니다. 대신 새 모듈 {{loopback-boot}}을(를) 사용하십시오. ",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0}은(는) 더 이상 사용되지 않습니다. 자세한 정보는 {1}을(를) 참조하십시오. ",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\"의 비오브젝트 \"methods\" 설정 무시",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "알 수 없는 \"{0}\" {{id}} \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 삭제했습니다. {0}",
+ "ea63d226b6968e328bdf6876010786b5": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 삭제된 레코드 수를 제대로 보고하지 않습니다. ",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{KeyValueModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t 수신인:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 전송:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "결과: {0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "충돌",
+ "f58cdc481540cd1f69a4aa4da2e37981": "올바르지 않은 비밀번호: {0}"
+}
+
diff --git a/intl/nl/messages.json b/intl/nl/messages.json
new file mode 100644
index 000000000..080ed46e1
--- /dev/null
+++ b/intl/nl/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "Geen wijzigingsrecord gevonden voor {0} met ID {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "Voor verificatie moet model {0} worden gedefinieerd.",
+ "0731d0109e46c21a4e34af3346ed4856": "Dit gedrag kan gewijzigd worden in de volgende hoofdversie.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Toegang geweigerd",
+ "0caffe1d763c8cca6a61814abe33b776": "E-mail is vereist",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "De lopende tekst van het antwoord bevat eigenschappen van het {{AccessToken}} dat is gemaakt bij aanmelding.\nAfhankelijk van de waarde van de parameter 'include' kan de lopende tekst aanvullende eigenschappen bevatten:\n\n - 'user' - 'U+007BUserU+007D' - Gegevens van de aangemelde gebruiker. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t ONDERWERP: {0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Gemaakt: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t VAN: {0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "kan {0} met ID {1} niet vinden",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0}-middleware is gedeprecieerd. Zie {1} voor meer informatie.",
+ "308e1d484516a33df788f873e65faaff": "Model '{0}' is een uitbreiding van het gedeprecieerde 'DataModel'. Gebruik in plaats daarvan 'PersistedModel'.",
+ "316e5b82c203cf3de31a449ee07d0650": "Booleaanse waarde verwacht, {0} ontvangen",
+ "320c482401afa1207c04343ab162e803": "Ongeldig type principal: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "De relaties-eigenschap van de '{0}'-configuratie moet een object zijn",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Controleer uw e-mail door deze link te openen in een webbrowser:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "Aanmelding mislukt omdat e-mail niet is gecontroleerd",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Onbekende {{middleware}}-fase {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "Ongeldig token: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Hartelijk dank voor uw registratie",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Waarschuwing: Geen e-mailtransport opgegeven voor verzending van e-mail. Configureer een transport om e-mailberichten te verzenden.",
+ "4203ab415ec66a78d3164345439ba76e": "{0} kan niet worden aangeroepen. {1}(). De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail is niet gevonden",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Mail verzenden:",
+ "4b494de07f524703ac0879addbd64b13": "E-mail is niet geverifieerd",
+ "4cac5f051ae431321673e04045d37772": "Model '{0}' is een uitbreiding van onbekend model '{1}'. 'PersistedModel' wordt gebruikt als basis.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Gegevensbron {0}: {1} kan n iet worden gemaakt",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "U moet verbinding maken tussen het model {{Email}} en een {{Mail}}-connector",
+ "5e81ad3847a290dc650b47618b9cbc7e": "Aanmelden is mislukt",
+ "5fa3afb425819ebde958043e598cb664": "geen model gevonden met {{id}} {0}",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "Relatie '{0}' voor model '{1}' bestaat niet",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal bijgewerkte records.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TEKST: {0}",
+ "6bc376432cd9972cf991aad3de371e78": "Ontbrekende gegevens voor wijziging: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} is niet gevonden",
+ "734a7bebb65e10899935126ba63dd51f": "De opties-eigenschap van de '{0}'-configuratie moet een object zijn",
+ "779467f467862836e19f494a37d6ab77": "De acls-eigenschap van de '{0}'-configuratie moet een array objecten zijn",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "Mijn eerste mobiele toepassing",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Ongeldig toegangstoken",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Verplichte verificatie",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is vereist voor afmelding",
+ "80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschap '{0}' mag niet opnieuw worden geconfigureerd voor '{1}'",
+ "855ecd4a64885ba272d782435f72a4d4": "Onbekend \"{0}\"-ID \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "Transport biedt geen ondersteuning voor HTTP-omleidingen.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} of {{email}} is verplicht",
+ "8a17c5ef611e2e7535792316e66b8fca": "Wachtwoord is te lang: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Aanvraag voor host {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Ongeldige niet-lokale methode: '{0}'",
+ "8bab6720ecc58ec6412358c858a53484": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewijzigd: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML: {0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "De eigenschap {{`dataSource`}} ontbreekt in de configuratie van '{0}'.\nGebruik 'null' of 'false' om modellen te markeren die niet gekoppeld zijn aan een gegevensbron.",
+ "a40684f5a9f546115258b76938d1de37": "Een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "Gebruiker is niet gevonden: {0}",
+ "a80038252430df2754884bf3c845c4cf": "Vlag \"isStatic\" ontbreekt in remoting (externe) metagegevens voor \"{0}.{1}\"; de methode wordt geregistreerd als instancemethode.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "Onbekend \"{0}\" {{key}} \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is verplicht",
+ "c2b5d51f007178170ca3952d59640ca4": "Wijzigingen van {0} kunnen niet worden hersteld:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "U moet een geldig e-mailadres opgeven",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "U moet een {{id}} of {{data}} opgeven",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} is verwijderd; gebruik in plaats daarvan de nieuwe module {{loopback-boot}}",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} is gedeprecieerd. Zie {1} voor meer informatie.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "Niet-object \"methods\"-instelling \"{0}\" wordt genegeerd.",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Onbekend \"{0}\" {{id}} \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewist: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal gewiste records.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "{0} kan niet worden aangeroepen. {1}(). De methode {2} is niet geconfigureerd. De {{KeyValueModel}} is niet correct gekoppeld aan een {{DataSource}}!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t AAN: {0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT: {0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "resultaat:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Conflict",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Ongeldige wachtwoord: {0}"
+}
+
diff --git a/intl/pt/messages.json b/intl/pt/messages.json
new file mode 100644
index 000000000..d5698b4e6
--- /dev/null
+++ b/intl/pt/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "Nenhum registro de mudança localizado para {0} com o ID {1}",
+ "04bd8af876f001ceaf443aad6a9002f9": "Autenticação requer que modelo {0} seja definido.",
+ "0731d0109e46c21a4e34af3346ed4856": "Este comportamento pode mudar na próxima versão principal.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Acesso Negado",
+ "0caffe1d763c8cca6a61814abe33b776": "E-mail é necessário",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "uma lista de cores está disponível em {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "O corpo de resposta contém propriedades do {{AccessToken}} criado no login.\nDependendo do valor do parâmetro `include`, o corpo poderá conter propriedades adicionais:\n\n - `user` - `U+007BUserU+007D` - Dados do usuário com login efetuado atualmente. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t ASSUNTO:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Criado: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t DE:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "não foi possível localizar {0} com ID {1}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "O middleware {0} foi descontinuado. Consulte {1} para obter mais detalhes.",
+ "308e1d484516a33df788f873e65faaff": "O modelo `{0}` está estendendo `DataModel` descontinuado. Use `PersistedModel` no lugar.",
+ "316e5b82c203cf3de31a449ee07d0650": "Booleano esperado, obteve {0}",
+ "320c482401afa1207c04343ab162e803": "Tipo principal inválido: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "A propriedade de relações da configuração de `{0}` deve ser um objeto",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Verifique seu e-mail abrindo este link em um navegador da web:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "login com falha pois o e-mail não foi verificado",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Fase {0} do {{middleware}} desconhecida",
+ "3caaa84fc103d6d5612173ae6d43b245": "Token inválido: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Obrigado por se Registrar",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Aviso: Nenhum transporte de e-mail especificado para enviar e-mail. Configure um transporte para enviar mensagens de e-mail.",
+ "4203ab415ec66a78d3164345439ba76e": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{PersistedModel}} não foi conectado corretamente a uma {{DataSource}}!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail não encontrado",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando E-mail:",
+ "4b494de07f524703ac0879addbd64b13": "E-mail não foi verificado",
+ "4cac5f051ae431321673e04045d37772": "O modelo `{0}` está estendendo um modelo `{1}` desconhecido. Usando `PersistedModel` como a base.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Não é possível criar origem de dados {0}: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "Deve-se conectar o Modelo de {{Email}} em um conector de {{Mail}}",
+ "5e81ad3847a290dc650b47618b9cbc7e": "falha de login",
+ "5fa3afb425819ebde958043e598cb664": "não foi possível localizar um modelo com {{id}} {0}",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "Relação `{0}` não existe para o modelo `{1}`",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros de atualização corretamente.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Dados ausentes para a mudança: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "Não foi possível localizar o {{accessToken}}",
+ "734a7bebb65e10899935126ba63dd51f": "A propriedade de opções da configuração de `{0}` deve ser um objeto",
+ "779467f467862836e19f494a37d6ab77": "A propriedade acls da configuração de `{0}` deve ser uma matriz de objetos",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "Meu primeiro aplicativo móvel",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Token de Acesso Inválido",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Autorização Necessária",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} é necessário para efetuar logout",
+ "80a32e80cbed65eba2103201a7c94710": "Modelo não localizado: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "A propriedade `{0}` não pode ser reconfigurada para `{1}`",
+ "855ecd4a64885ba272d782435f72a4d4": "ID \"{1}\" de \"{0}\" desconhecido.",
+ "860d1a0b8bd340411fb32baa72867989": "O transporte não suporta redirecionamentos de HTTP.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} é necessário",
+ "8a17c5ef611e2e7535792316e66b8fca": "Senha muito longa: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "Solicitação para o host {0}",
+ "8ae418c605b6a45f2651be9b1677c180": "Método remoto inválido: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "Atualização em massa falhou, o conector modificou um número inesperado de registros: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "A configuração de `{0}` não possui a propriedade {{`dataSource`}}.\nUse `null` ou `false` para marcar modelos não conectados a nenhuma origem de dados.",
+ "a40684f5a9f546115258b76938d1de37": "Uma lista de cores está disponível em {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "Usuário não localizado: {0}",
+ "a80038252430df2754884bf3c845c4cf": "Metadados remotos para \"{0}.{1}\" não possui sinalização \" isStatic\", o método foi registrado como um método de instância.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" desconhecido.",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} é obrigatório",
+ "c2b5d51f007178170ca3952d59640ca4": "Não é possível retificar mudanças de {0}:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "Deve-se fornecer um e-mail válido",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "deve-se especificar um {{id}} ou {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} foi removido, use o novo módulo {{loopback-boot}} no lugar",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} foi descontinuado. Consulte {1} para obter mais detalhes.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "Ignorando configuração de \"methods\" de não objeto de \"{0}\".",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" desconhecido.",
+ "e92aa25b6b864e3454b65a7c422bd114": "Atualização em massa falhou, o conector excluiu um número inesperado de registros: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros excluídos corretamente.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{KeyValueModel}} não foi conectado corretamente a uma {{DataSource}}!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t PARA:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Conflito",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Senha inválida: {0}"
+}
+
diff --git a/intl/tr/messages.json b/intl/tr/messages.json
new file mode 100644
index 000000000..e56e50381
--- /dev/null
+++ b/intl/tr/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "{0} için {1} tanıtıcılı bir değişiklik kaydı bulunamadı",
+ "04bd8af876f001ceaf443aad6a9002f9": "Kimlik doğrulaması {0} modelinin tanımlanmasını gerektiriyor.",
+ "0731d0109e46c21a4e34af3346ed4856": "Bu davranış sonraki ana sürümde değişebilir.",
+ "095afbf2f1f0e5be678f5dac5c54e717": "Erişim Verilmedi",
+ "0caffe1d763c8cca6a61814abe33b776": "E-posta zorunludur",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "Yanıt gövdesi, oturum açma sırasında yaratılan {{AccessToken}} belirtecine ilişkin özellikleri içerir.\n`include` parametresinin değerine bağlı olarak, gövde ek özellikler içerebilir:\n\n - `user` - `U+007BUserU+007D` - Oturum açmış olan kullanıcıya ilişkin veriler. {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t KONU:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "Yaratıldığı tarih: {0}",
+ "275f22ab95671f095640ca99194b7635": "\t KİMDEN:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "{1} tanıtıcılı {0} bulunamadı",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0} ara katman yazılımı kullanım dışı bırakıldı. Daha fazla ayrıntı için bkz. {1}.",
+ "308e1d484516a33df788f873e65faaff": "`{0}` modeli kullanım dışı bırakılmış şu modeli genişletiyor: `DataModel. Onun yerine `PersistedModel` modelini kullanın.",
+ "316e5b82c203cf3de31a449ee07d0650": "Boole beklenirken {0} alındı",
+ "320c482401afa1207c04343ab162e803": "Geçersiz birincil kullanıcı tipi: {0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` yapılandırmasının ilişkiler (relations) özelliği bir nesne olmalıdır",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "Lütfen bu bağlantıyı bir web tarayıcısında açarak e-postanızı doğrulayın:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "e-posta doğrulanmadığından oturum açma başarısız oldu",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "Bilinmeyen {{middleware}} aşaması {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "Geçersiz belirteç: {0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "Kaydolduğunuz için teşekkürler",
+ "3d63008ccfb2af1db2142e8cc2716ace": "Uyarı: E-posta göndermek için e-posta aktarımı belirtilmedi. Posta iletileri göndermek için aktarım ayarlayın.",
+ "4203ab415ec66a78d3164345439ba76e": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{PersistedModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "E-posta bulunamadı",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "Posta Gönderiliyor:",
+ "4b494de07f524703ac0879addbd64b13": "E-posta doğrulanmadı",
+ "4cac5f051ae431321673e04045d37772": "`{0}` modeli, bilinmeyen `{1}` modelini genişletiyor. Temel olarak 'PersistedModel' kullanılıyor.",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Veri kaynağı {0} yaratılamıyor: {1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} modelini bir {{Mail}} bağlayıcısına bağlamalısınız",
+ "5e81ad3847a290dc650b47618b9cbc7e": "oturum açma başarısız oldu",
+ "5fa3afb425819ebde958043e598cb664": "{{id}} {0} tanıtıcılı bir model bulunamadı",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "`{1}` modeli için `{0}` ilişkisi yok",
+ "62e8b0a733417978bab22c8dacf5d7e6": "Toplu güncelleme uygulanamaz; bağlayıcı, güncellenen kayıtların sayısını doğru olarak bildirmiyor.",
+ "63a091ced88001ab6acb58f61ec041c5": "\t METİN:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "Değişiklik için veri eksik: {0}",
+ "705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} bulunamadı",
+ "734a7bebb65e10899935126ba63dd51f": "`{0}` yapılandırmasının seçenekler (options) özelliği bir nesne olmalıdır.",
+ "779467f467862836e19f494a37d6ab77": "`{0}` yapılandırmasının erişim denetim listeleri (acls) özelliği bir nesne dizisi olmalıdır.",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "İlk mobil uygulamam",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "Geçersiz Erişim Belirteci",
+ "7e287fc885d9fdcf42da3a12f38572c1": "Yetkilendirme Gerekli",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "Oturumu kapatmak için {{accessToken}} zorunludur",
+ "80a32e80cbed65eba2103201a7c94710": "Model bulunamadı: {0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "`{0}` özelliği `{1}` için yeniden yapılandırılamıyor",
+ "855ecd4a64885ba272d782435f72a4d4": "Bilinmeyen \"{0}\" tanıtıcısı \"{1}\".",
+ "860d1a0b8bd340411fb32baa72867989": "Aktarım HTTP yeniden yönlendirmelerini desteklemiyor.",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} ya da {{email}} zorunludur",
+ "8a17c5ef611e2e7535792316e66b8fca": "Parola çok uzun: {0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "{0} ana makinesine yönelik istek",
+ "8ae418c605b6a45f2651be9b1677c180": "Uzak yöntem geçersiz: `{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı değiştirdi: {0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` yapılandırmasında {{`dataSource`}} özelliği eksik.\nHiçbir veri kaynağına eklenmemiş modelleri işaretlemek için `null` ya da `false` kullanın.",
+ "a40684f5a9f546115258b76938d1de37": "Renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "Kullanıcı bulunamadı: {0}",
+ "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" ile ilgili uzaktan iletişim meta verisinde \"isStatic\" işareti eksik; yöntem bir eşgörünüm yöntemi olarak kaydedildi.",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "Bilinmeyen \"{0}\" {{key}} \"{1}\".",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} zorunludur",
+ "c2b5d51f007178170ca3952d59640ca4": "{0} değişiklik düzeltilemiyor:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "Geçerli bir e-posta belirtilmeli",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "bir {{id}} ya da {{data}} belirtmelidir",
+ "d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} kaldırıldı, onun yerine yeni {{loopback-boot}} modülünü kullanın",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} kullanım dışı bırakıldı. Daha fazla ayrıntı için bkz. {1}.",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" öğesinin nesne olmayan \"methods\" atarı yoksayılıyor.",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "Bilinmeyen \"{0}\" {{id}} \"{1}\".",
+ "e92aa25b6b864e3454b65a7c422bd114": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı sildi: {0}",
+ "ea63d226b6968e328bdf6876010786b5": "Toplu güncelleme uygulanamaz; bağlayıcı, silinen kayıtların sayısını doğru olarak bildirmiyor.",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "Çağrılamıyor: {0}.{1}(). {2} yöntemi ayarlanmamış. {{KeyValueModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t KİME:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t AKTARIM:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "sonuç:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "Çakışma",
+ "f58cdc481540cd1f69a4aa4da2e37981": "Geçersiz parola: {0}"
+}
+
diff --git a/intl/zh-Hans/messages.json b/intl/zh-Hans/messages.json
new file mode 100644
index 000000000..8cfebffef
--- /dev/null
+++ b/intl/zh-Hans/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "对于标识为 {1} 的 {0},找不到任何更改记录",
+ "04bd8af876f001ceaf443aad6a9002f9": "认证需要定义模型 {0}。",
+ "0731d0109e46c21a4e34af3346ed4856": "在下一个主版本中,此行为可能进行更改。",
+ "095afbf2f1f0e5be678f5dac5c54e717": "拒绝访问",
+ "0caffe1d763c8cca6a61814abe33b776": "电子邮件是必需的",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "颜色列表位于:{{http://localhost:3000/colors}}",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "响应主体包含在登录时创建的 {{AccessToken}} 的属性。\n根据“include”参数的值,主体可包含其他属性:\n\n - `user` - `U+007BUserU+007D` - 当前已登录用户的数据。 {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t主题:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "创建时间:{0}",
+ "275f22ab95671f095640ca99194b7635": "\t发件人:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "无法找到标识为 {1} 的 {0}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "不推荐 {0} 中间件。请参阅 {1} 以获取更多详细信息。",
+ "308e1d484516a33df788f873e65faaff": "模型“{0}”正在扩展不推荐使用的“DataModel”。请改用“PersistedModel”。",
+ "316e5b82c203cf3de31a449ee07d0650": "期望布尔值,获取 {0}",
+ "320c482401afa1207c04343ab162e803": "无效的主体类型:{0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "“{0}”配置的关系属性必须是对象。",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "请通过在 Web 浏览器中打开此链接来验证您的电子邮件:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "因为尚未验证电子邮件,登录失败",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "未知的 {{middleware}} 阶段 {0}",
+ "3caaa84fc103d6d5612173ae6d43b245": "无效的令牌:{0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "感谢您注册",
+ "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用于发送电子邮件的电子邮件传输。设置传输以发送电子邮件消息。",
+ "4203ab415ec66a78d3164345439ba76e": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{PersistedModel}} 未正确附加到 {{DataSource}}!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到电子邮件",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在发送电子邮件:",
+ "4b494de07f524703ac0879addbd64b13": "尚未验证电子邮件",
+ "4cac5f051ae431321673e04045d37772": "模型“{0}”正在扩展未知的模型“{1}”。使用“PersistedModel”作为基础。",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "无法创建数据源 {0}:{1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "您必须将 {{Email}} 模型连接到 {{Mail}} 连接器",
+ "5e81ad3847a290dc650b47618b9cbc7e": "登录失败",
+ "5fa3afb425819ebde958043e598cb664": "找不到具有 {{id}} {0} 的模型",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "对于模型“{1}”,关系“{0}”不存在",
+ "62e8b0a733417978bab22c8dacf5d7e6": "无法应用批量更新,连接器未正确报告更新的记录数。",
+ "63a091ced88001ab6acb58f61ec041c5": "\t 文本:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "缺少更改的数据:{0}",
+ "705c2d456a3e204c4af56e671ec3225c": "无法找到 {{accessToken}}",
+ "734a7bebb65e10899935126ba63dd51f": "“{0}”配置的选项属性必须是对象。",
+ "779467f467862836e19f494a37d6ab77": "“{0}”配置的 acls 属性必须是对象数组。",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一个移动应用程序",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "无效的访问令牌",
+ "7e287fc885d9fdcf42da3a12f38572c1": "需要授权",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} 需要注销",
+ "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "无法针对“{1}”重新配置属性“{0}”。",
+ "855ecd4a64885ba272d782435f72a4d4": "未知的“{0}”标识“{1}”。",
+ "860d1a0b8bd340411fb32baa72867989": "传输不支持 HTTP 重定向。",
+ "895b1f941d026870b3cc8e6af087c197": "{{username}} 或 {{email}} 是必需的",
+ "8a17c5ef611e2e7535792316e66b8fca": "密码过长:{0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "到主机 {0} 的请求",
+ "8ae418c605b6a45f2651be9b1677c180": "无效的远程方法:“{0}”",
+ "8bab6720ecc58ec6412358c858a53484": "批量更新失败,连接器已修改意外数量的记录:{0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "“{0}”的配置缺少 {{`dataSource`}} 属性。\n使用“null”或“false”来标记未附加到任何数据源的模型。",
+ "a40684f5a9f546115258b76938d1de37": "颜色列表位于:{{http://localhost:3000/colors}}",
+ "a50d10fc6e0959b220e085454c40381e": "找不到用户:{0}",
+ "a80038252430df2754884bf3c845c4cf": "“{0}.{1}”的远程处理元数据缺少“isStatic”标志,方法注册为实例方法。",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "未知的“{0}”{{key}}“{1}”。",
+ "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} 是必需的",
+ "c2b5d51f007178170ca3952d59640ca4": "无法纠正 {0} 更改:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "必须提供有效电子邮件",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "必须指定 {{id}} 或 {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "已除去 {{`app.boot`}},请改用新模块 {{loopback-boot}}",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "不推荐使用 {0}。请参阅 {1} 以获取更多详细信息。",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略“{0}”的非对象“方法”设置。",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "未知的“{0}”{{id}}“{1}”。",
+ "e92aa25b6b864e3454b65a7c422bd114": "批量更新失败,连接器已删除意外数量的记录:{0}",
+ "ea63d226b6968e328bdf6876010786b5": "无法应用批量更新,连接器未正确报告删除的记录数。",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{KeyValueModel}} 未正确附加到 {{DataSource}}!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件人:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 传输:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "结果:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "冲突",
+ "f58cdc481540cd1f69a4aa4da2e37981": "无效的密码:{0}"
+}
+
diff --git a/intl/zh-Hant/messages.json b/intl/zh-Hant/messages.json
new file mode 100644
index 000000000..a1d13ec88
--- /dev/null
+++ b/intl/zh-Hant/messages.json
@@ -0,0 +1,76 @@
+{
+ "03f79fa268fe199de2ce4345515431c1": "對於 id 為 {1} 的 {0},找不到變更記錄",
+ "04bd8af876f001ceaf443aad6a9002f9": "需要定義模型 {0} 才能鑑別。",
+ "0731d0109e46c21a4e34af3346ed4856": "下一個主要版本中可能會變更這個行為。",
+ "095afbf2f1f0e5be678f5dac5c54e717": "拒絕存取",
+ "0caffe1d763c8cca6a61814abe33b776": "需要電子郵件",
+ "10e01c895dc0b2fecc385f9f462f1ca6": "{{http://localhost:3000/colors}} 提供顏色清單",
+ "1b2a6076dccbe91a56f1672eb3b8598c": "回應內文包含登入時建立的 {{AccessToken}} 的內容。\n根據 `include` 參數的值而定,內文還可能包含其他內容:\n\n - `user` - `U+007BUserU+007D` - 目前登入的使用者的資料。 {{(`include=user`)}}\n\n",
+ "1d7833c3ca2f05fdad8fad7537531c40": "\t 主旨:{0}",
+ "1e85f822b547a75d7d385048030e4ecb": "已建立:{0}",
+ "275f22ab95671f095640ca99194b7635": "\t 寄件者:{0}",
+ "2d3071e3b18681c80a090dc0efbdb349": "找不到 id 為 {1} 的 {0}",
+ "2d78192c43fd2ec52ec18f3918894f9a": "{0} 中介軟體已淘汰。如需詳細資料,請參閱 {1}。",
+ "308e1d484516a33df788f873e65faaff": "模型 `{0}` 正在延伸已淘汰的 `DataModel。請改用 `PersistedModel`。",
+ "316e5b82c203cf3de31a449ee07d0650": "預期為布林,但卻取得 {0}",
+ "320c482401afa1207c04343ab162e803": "無效的主體類型:{0}",
+ "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 配置的 relations 內容必須是物件",
+ "35e5252c62d80f8c54a5290d30f4c7d0": "請在 Web 瀏覽器中開啟此鏈結來驗證電子郵件:\n\t{0}",
+ "3aae63bb7e8e046641767571c1591441": "因為尚未驗證電子郵件,所以登入失敗",
+ "3aecb24fa8bdd3f79d168761ca8a6729": "{{middleware}} 階段 {0} 不明",
+ "3caaa84fc103d6d5612173ae6d43b245": "無效記號:{0}",
+ "3d617953470be16d0c2b32f0bcfbb5ee": "感謝您登錄",
+ "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用於傳送電子郵件的電子郵件傳輸。請設定傳輸來傳送郵件訊息。",
+ "4203ab415ec66a78d3164345439ba76e": "無法呼叫 {0}.{1}()。尚未設定 {2} 方法。{{PersistedModel}} 未正確連接至 {{DataSource}}!",
+ "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到電子郵件",
+ "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在傳送郵件:",
+ "4b494de07f524703ac0879addbd64b13": "尚未驗證電子郵件",
+ "4cac5f051ae431321673e04045d37772": "模型 `{0}` 正在延伸不明模型 `{1}`。請使用 `PersistedModel` 作為基礎。",
+ "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "無法建立資料來源 {0}:{1}",
+ "5858e63efaa0e4ad86b61c0459ea32fa": "您必須將 {{Email}} 模型連接至 {{Mail}} 連接器",
+ "5e81ad3847a290dc650b47618b9cbc7e": "登入失敗",
+ "5fa3afb425819ebde958043e598cb664": "找不到 {{id}} 為 {0} 的模型",
+ "61e5deebaf44d68f4e6a508f30cc31a3": "模型 `{1}` 的關係 `{0}` 不存在",
+ "62e8b0a733417978bab22c8dacf5d7e6": "無法套用大量更新,連接器未正確報告已更新的記錄數。",
+ "63a091ced88001ab6acb58f61ec041c5": "\t 文字:{0}",
+ "6bc376432cd9972cf991aad3de371e78": "遺漏變更的資料:{0}",
+ "705c2d456a3e204c4af56e671ec3225c": "找不到 {{accessToken}}",
+ "734a7bebb65e10899935126ba63dd51f": "`{0}` 配置的 options 內容必須是物件",
+ "779467f467862836e19f494a37d6ab77": "`{0}` 配置的 acls 內容必須是物件陣列",
+ "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一個行動式應用程式",
+ "7e0fca41d098607e1c9aa353c67e0fa1": "存取記號無效",
+ "7e287fc885d9fdcf42da3a12f38572c1": "需要授權",
+ "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "需要 {{accessToken}} 才能登出",
+ "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}",
+ "83cbdc2560ba9f09155ccfc63e08f1a1": "無法為 `{1}` 重新配置內容 `{0}`",
+ "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" 不明。",
+ "860d1a0b8bd340411fb32baa72867989": "傳輸不支援 HTTP 重新導向。",
+ "895b1f941d026870b3cc8e6af087c197": "需要 {{username}} 或 {{email}}",
+ "8a17c5ef611e2e7535792316e66b8fca": "密碼太長:{0}",
+ "8a27e0c9ce3ebf0e0c3978efb456e13e": "向主機 {0} 要求",
+ "8ae418c605b6a45f2651be9b1677c180": "無效的遠端方法:`{0}`",
+ "8bab6720ecc58ec6412358c858a53484": "大量更新失敗,連接器已修改超乎預期的記錄數:{0}",
+ "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
+ "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` 的配置遺漏 {{`dataSource`}} 內容。\n請使用 `null` 或 `false` 來標示未連接至任何資料來源的模型。",
+ "a40684f5a9f546115258b76938d1de37": "{{http://localhost:3000/colors}} 提供顏色清單",
+ "a50d10fc6e0959b220e085454c40381e": "找不到使用者:{0}",
+ "a80038252430df2754884bf3c845c4cf": "\"{0}.{1}\" 的遠端 meta 資料遺漏 \"isStatic\" 旗標,這個方法已登錄為實例方法。",
+ "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" 不明。",
+ "ba96498b10c179f9cd75f75c8def4f70": "需要 {{realm}}",
+ "c2b5d51f007178170ca3952d59640ca4": "無法更正 {0} 個變更:\n{1}",
+ "c68a93f0a9524fed4ff64372fc90c55f": "必須提供有效的電子郵件",
+ "cd0412f2f33a4a2a316acc834f3f21a6": "必須指定 {{id}} 或 {{data}}",
+ "d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},請改用新的模組 {{loopback-boot}}",
+ "d9ef6dc3770dd8f80a129e92a79851f3": "{0} 已淘汰。如需詳細資料,請參閱 {1}。",
+ "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略 \"{0}\" 的非物件 \"methods\" 設定。",
+ "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" 不明。",
+ "e92aa25b6b864e3454b65a7c422bd114": "大量更新失敗,連接器已刪除非預期的記錄數:{0}",
+ "ea63d226b6968e328bdf6876010786b5": "無法套用大量更新,連接器未正確報告已刪除的記錄數。",
+ "ead044e2b4bce74b4357f8a03fb78ec4": "無法呼叫 {0}.{1}()。尚未設定 {2} 方法。{{KeyValueModel}} 未正確連接至 {{DataSource}}!",
+ "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件者:{0}",
+ "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 傳輸:{0}",
+ "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}",
+ "f1d4ac54357cc0932f385d56814ba7e4": "衝突",
+ "f58cdc481540cd1f69a4aa4da2e37981": "無效密碼:{0}"
+}
+
diff --git a/lib/access-context.js b/lib/access-context.js
index 75ec50165..21aa7fc5b 100644
--- a/lib/access-context.js
+++ b/lib/access-context.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var assert = require('assert');
var loopback = require('./loopback');
var debug = require('debug')('loopback:security:access-context');
@@ -56,16 +61,16 @@ function AccessContext(context) {
var principalType = context.principalType || Principal.USER;
var principalId = context.principalId || undefined;
var principalName = context.principalName || undefined;
- if (principalId) {
+ if (principalId != null) {
this.addPrincipal(principalType, principalId, principalName);
}
var token = this.accessToken || {};
- if (token.userId) {
+ if (token.userId != null) {
this.addPrincipal(Principal.USER, token.userId);
}
- if (token.appId) {
+ if (token.appId != null) {
this.addPrincipal(Principal.APPLICATION, token.appId);
}
this.remotingContext = context.remotingContext;
@@ -146,7 +151,7 @@ AccessContext.prototype.getAppId = function() {
* @returns {boolean}
*/
AccessContext.prototype.isAuthenticated = function() {
- return !!(this.getUserId() || this.getAppId());
+ return this.getUserId() != null || this.getAppId() != null;
};
/*!
diff --git a/lib/application.js b/lib/application.js
index bd3375539..ea1049606 100644
--- a/lib/application.js
+++ b/lib/application.js
@@ -1,7 +1,14 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module dependencies.
*/
+var g = require('strong-globalize')();
+
var DataSource = require('loopback-datasource-juggler').DataSource;
var Registry = require('./registry');
var assert = require('assert');
@@ -153,6 +160,11 @@ app.model = function(Model, config) {
this.emit('modelRemoted', Model.sharedClass);
}
+ var self = this;
+ Model.on('remoteMethodDisabled', function(model, methodName) {
+ self.emit('remoteMethodDisabled', model, methodName);
+ });
+
Model.shared = isPublic;
Model.app = this;
Model.emit('attached', this);
@@ -219,11 +231,20 @@ app.models = function() {
* @param {Object} config The data source config
*/
app.dataSource = function(name, config) {
- var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
- this.dataSources[name] =
- this.dataSources[classify(name)] =
- this.dataSources[camelize(name)] = ds;
- return ds;
+ try {
+ var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
+ this.dataSources[name] =
+ this.dataSources[classify(name)] =
+ this.dataSources[camelize(name)] = ds;
+ ds.app = this;
+ return ds;
+ } catch (err) {
+ if (err.message) {
+ err.message = g.f('Cannot create data source %s: %s',
+ JSON.stringify(name), err.message);
+ }
+ throw err;
+ }
};
/**
@@ -304,11 +325,13 @@ app.enableAuth = function(options) {
var Model = app.registry.findModel(m);
if (!Model) {
throw new Error(
- 'Authentication requires model ' + m + ' to be defined.');
+ g.f('Authentication requires model %s to be defined.', m));
}
- if (m.dataSource || m.app) return;
+ if (Model.dataSource || Model.app) return;
+ // Find descendants of Model that are attached,
+ // for example "Customer" extending "User" model
for (var name in appModels) {
var candidate = appModels[name];
var isSubclass = candidate.prototype instanceof Model;
@@ -360,17 +383,17 @@ app.enableAuth = function(options) {
var messages = {
403: {
- message: 'Access Denied',
- code: 'ACCESS_DENIED'
+ message: g.f('Access Denied'),
+ code: 'ACCESS_DENIED',
},
404: {
- message: ('could not find ' + modelName + ' with id ' + modelId),
- code: 'MODEL_NOT_FOUND'
+ message: (g.f('could not find %s with id %s', modelName, modelId)),
+ code: 'MODEL_NOT_FOUND',
},
401: {
- message: 'Authorization Required',
- code: 'AUTHORIZATION_REQUIRED'
- }
+ message: g.f('Authorization Required'),
+ code: 'AUTHORIZATION_REQUIRED',
+ },
};
var e = new Error(messages[errStatusCode].message || messages[403].message);
@@ -390,14 +413,14 @@ app.enableAuth = function(options) {
app.boot = function(options) {
throw new Error(
- '`app.boot` was removed, use the new module loopback-boot instead');
+ g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead'));
};
function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
var connectorPath;
assert(typeof config === 'object',
- 'cannont create data source without config object');
+ 'can not create data source without config object');
if (typeof config.connector === 'string') {
name = config.connector;
@@ -410,6 +433,8 @@ function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
config.connector = require(connectorPath);
}
}
+ if (!config.connector.name)
+ config.connector.name = name;
}
return registry.createDataSource(config);
@@ -466,7 +491,7 @@ function setSharedMethodSharedProperties(model, app, modelConfigs) {
var settingValue = settings[setting];
var settingValueType = typeof settingValue;
if (settingValueType !== 'boolean')
- throw new TypeError('Expected boolean, got ' + settingValueType);
+ throw new TypeError(g.f('Expected boolean, got %s', settingValueType));
});
// set sharedMethod.shared using the merged settings
@@ -551,7 +576,11 @@ app.listen = function(cb) {
(arguments.length == 1 && typeof arguments[0] == 'function');
if (useAppConfig) {
- server.listen(this.get('port'), this.get('host'), cb);
+ var port = this.get('port');
+ // NOTE(bajtos) port:undefined no longer works on node@6,
+ // we must pass port:0 explicitly
+ if (port === undefined) port = 0;
+ server.listen(port, this.get('host'), cb);
} else {
server.listen.apply(server, arguments);
}
diff --git a/lib/browser-express.js b/lib/browser-express.js
index 2a7dbe912..3b4237202 100644
--- a/lib/browser-express.js
+++ b/lib/browser-express.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var EventEmitter = require('events').EventEmitter;
var util = require('util');
diff --git a/lib/builtin-models.js b/lib/builtin-models.js
index 78bf63990..dc23b409f 100644
--- a/lib/builtin-models.js
+++ b/lib/builtin-models.js
@@ -1,6 +1,15 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
module.exports = function(registry) {
// NOTE(bajtos) we must use static require() due to browserify limitations
+ registry.KeyValueModel = createModel(
+ require('../common/models/key-value-model.json'),
+ require('../common/models/key-value-model.js'));
+
registry.Email = createModel(
require('../common/models/email.json'),
require('../common/models/email.js'));
diff --git a/lib/connectors/base-connector.js b/lib/connectors/base-connector.js
index c1e37b7ba..a11dcce31 100644
--- a/lib/connectors/base-connector.js
+++ b/lib/connectors/base-connector.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/**
* Expose `Connector`.
*/
diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js
index a36984c39..735e30115 100644
--- a/lib/connectors/mail.js
+++ b/lib/connectors/mail.js
@@ -1,7 +1,14 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/**
* Dependencies.
*/
+var g = require('strong-globalize')();
+
var mailer = require('nodemailer');
var assert = require('assert');
var debug = require('debug')('loopback:connector:mail');
@@ -54,10 +61,11 @@ MailConnector.prototype.DataAccessObject = Mailer;
* Example:
*
* Email.setupTransport({
- * type: 'SMTP',
+ * type: "SMTP",
* host: "smtp.gmail.com", // hostname
* secureConnection: true, // use SSL
* port: 465, // port for secure SMTP
+ * alias: "gmail", // optional alias for use with 'transport' option when sending
* auth: {
* user: "gmail.user@gmail.com",
* pass: "userpass"
@@ -83,7 +91,7 @@ MailConnector.prototype.setupTransport = function(setting) {
transport = mailer.createTransport(transportModule(setting));
}
- connector.transportsIndex[setting.type] = transport;
+ connector.transportsIndex[setting.alias || setting.type] = transport;
connector.transports.push(transport);
};
@@ -122,7 +130,8 @@ MailConnector.prototype.defaultTransport = function() {
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
* subject: "Hello ✔", // Subject line
* text: "Hello world ✔", // plaintext body
- * html: "Hello world ✔" // html body
+ * html: "Hello world ✔", // html body
+ * transport: "gmail", // See 'alias' option above in setupTransport
* }
*
* See https://github.com/andris9/Nodemailer for other supported options.
@@ -144,22 +153,22 @@ Mailer.send = function(options, fn) {
}
if (debug.enabled || settings && settings.debug) {
- console.log('Sending Mail:');
+ g.log('Sending Mail:');
if (options.transport) {
- console.log('\t TRANSPORT:', options.transport);
+ console.log(g.f('\t TRANSPORT:%s', options.transport));
}
- console.log('\t TO:', options.to);
- console.log('\t FROM:', options.from);
- console.log('\t SUBJECT:', options.subject);
- console.log('\t TEXT:', options.text);
- console.log('\t HTML:', options.html);
+ g.log('\t TO:%s', options.to);
+ g.log('\t FROM:%s', options.from);
+ g.log('\t SUBJECT:%s', options.subject);
+ g.log('\t TEXT:%s', options.text);
+ g.log('\t HTML:%s', options.html);
}
if (transport) {
assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport');
transport.sendMail(options, fn);
} else {
- console.warn('Warning: No email transport specified for sending email.' +
+ g.warn('Warning: No email transport specified for sending email.' +
' Setup a transport to send mail messages.');
process.nextTick(function() {
fn(null, options);
diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js
index 6a34417cd..f62448f61 100644
--- a/lib/connectors/memory.js
+++ b/lib/connectors/memory.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/**
* Expose `Memory`.
*/
diff --git a/lib/current-context.js b/lib/current-context.js
new file mode 100644
index 000000000..185b7b16e
--- /dev/null
+++ b/lib/current-context.js
@@ -0,0 +1,78 @@
+// Copyright IBM Corp. 2015,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var juggler = require('loopback-datasource-juggler');
+var remoting = require('strong-remoting');
+var LoopBackContext = require('loopback-context');
+var deprecated = require('depd')('loopback');
+var g = require('strong-globalize')();
+
+module.exports = function(loopback) {
+
+ /**
+ * Get the current context object. The context is preserved
+ * across async calls, it behaves like a thread-local storage.
+ *
+ * @returns {ChainedContext} The context object or null.
+ */
+ loopback.getCurrentContext = function() {
+ // NOTE(bajtos) LoopBackContext.getCurrentContext is overriden whenever
+ // the context changes, therefore we cannot simply assign
+ // LoopBackContext.getCurrentContext() to loopback.getCurrentContext()
+ deprecated(g.f('%s is deprecated. See %s for more details.',
+ 'loopback.getCurrentContext()',
+ 'https://docs.strongloop.com/display/APIC/Using%20current%20context'));
+ return LoopBackContext.getCurrentContext();
+ };
+
+ juggler.getCurrentContext =
+ remoting.getCurrentContext = loopback.getCurrentContext;
+
+ /**
+ * Run the given function in such way that
+ * `loopback.getCurrentContext` returns the
+ * provided context object.
+ *
+ * **NOTE**
+ *
+ * The method is supported on the server only, it does not work
+ * in the browser at the moment.
+ *
+ * @param {Function} fn The function to run, it will receive arguments
+ * (currentContext, currentDomain).
+ * @param {ChainedContext} context An optional context object.
+ * When no value is provided, then the default global context is used.
+ */
+ loopback.runInContext = function(fn, ctx) {
+ deprecated(g.f('%s is deprecated. See %s for more details.',
+ 'loopback.runInContext()',
+ 'https://docs.strongloop.com/display/APIC/Using%20current%20context'));
+ return LoopBackContext.runInContext(fn, ctx);
+ };
+
+ /**
+ * Create a new LoopBackContext instance that can be used
+ * for `loopback.runInContext`.
+ *
+ * **NOTES**
+ *
+ * At the moment, `loopback.getCurrentContext` supports
+ * a single global context instance only. If you call `createContext()`
+ * multiple times, `getCurrentContext` will return the last context
+ * created.
+ *
+ * The method is supported on the server only, it does not work
+ * in the browser at the moment.
+ *
+ * @param {String} scopeName An optional scope name.
+ * @return {ChainedContext} The new context object.
+ */
+ loopback.createContext = function(scopeName) {
+ deprecated(g.f('%s is deprecated. See %s for more details.',
+ 'loopback.createContext()',
+ 'https://docs.strongloop.com/display/APIC/Using%20current%20context'));
+ return LoopBackContext.createContext(scopeName);
+ };
+};
diff --git a/lib/express-middleware.js b/lib/express-middleware.js
index f058a74a6..98fae0408 100644
--- a/lib/express-middleware.js
+++ b/lib/express-middleware.js
@@ -1,4 +1,10 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var path = require('path');
+var deprecated = require('depd')('loopback');
var middlewares = exports;
@@ -24,12 +30,10 @@ var middlewareModules = {
'cookieParser': 'cookie-parser',
'cookieSession': 'cookie-session',
'csrf': 'csurf',
- 'errorHandler': 'errorhandler',
'session': 'express-session',
'methodOverride': 'method-override',
'logger': 'morgan',
'responseTime': 'response-time',
- 'favicon': 'serve-favicon',
'directory': 'serve-index',
// 'static': 'serve-static',
'vhost': 'vhost'
@@ -39,14 +43,20 @@ middlewares.bodyParser = safeRequire('body-parser');
middlewares.json = middlewares.bodyParser && middlewares.bodyParser.json;
middlewares.urlencoded = middlewares.bodyParser && middlewares.bodyParser.urlencoded;
+['bodyParser', 'json', 'urlencoded'].forEach(function(name) {
+ if (!middlewares[name]) return;
+ middlewares[name] = deprecated.function(
+ middlewares[name],
+ deprecationMessage(name, 'body-parser'));
+});
+
for (var m in middlewareModules) {
var moduleName = middlewareModules[m];
middlewares[m] = safeRequire(moduleName) || createMiddlewareNotInstalled(m, moduleName);
+ deprecated.property(middlewares, m, deprecationMessage(m, moduleName));
}
-// serve-favicon requires a path
-var favicon = middlewares.favicon;
-middlewares.favicon = function(icon, options) {
- icon = icon || path.join(__dirname, '../favicon.ico');
- return favicon(icon, options);
-};
+function deprecationMessage(accessor, moduleName) {
+ return 'loopback.' + accessor + ' is deprecated. ' +
+ 'Use `require(\'' + moduleName + '\');` instead.';
+}
diff --git a/lib/loopback.js b/lib/loopback.js
index fd770e20d..3b3555393 100644
--- a/lib/loopback.js
+++ b/lib/loopback.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module dependencies.
*/
@@ -217,7 +222,7 @@ loopback.template = function(file) {
});
};
-require('../server/current-context')(loopback);
+require('../lib/current-context')(loopback);
/**
* Create a named vanilla JavaScript class constructor with an attached
diff --git a/lib/model.js b/lib/model.js
index f3ec47b25..89cd47c4d 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -1,10 +1,20 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module Dependencies.
*/
+
+var g = require('strong-globalize')();
+
var assert = require('assert');
+var debug = require('debug')('loopback:model');
var RemoteObjects = require('strong-remoting');
var SharedClass = require('strong-remoting').SharedClass;
var extend = require('util')._extend;
+var format = require('util').format;
module.exports = function(registry) {
@@ -116,31 +126,32 @@ module.exports = function(registry) {
var options = this.settings;
var typeName = this.modelName;
- var remotingOptions = {};
- extend(remotingOptions, options.remoting || {});
-
- // create a sharedClass
- var sharedClass = ModelCtor.sharedClass = new SharedClass(
- ModelCtor.modelName,
- ModelCtor,
- remotingOptions
- );
-
- // setup a remoting type converter for this model
- RemoteObjects.convert(typeName, function(val) {
- return val ? new ModelCtor(val) : val;
- });
-
// support remoting prototype methods
- ModelCtor.sharedCtor = function(data, id, fn) {
+ // it's important to setup this function *before* calling `new SharedClass`
+ // otherwise remoting metadata from our base model is picked up
+ ModelCtor.sharedCtor = function(data, id, options, fn) {
var ModelCtor = this;
- if (typeof data === 'function') {
+ var isRemoteInvocationWithOptions = typeof data !== 'object' &&
+ typeof id === 'object' &&
+ typeof options === 'function';
+ if (isRemoteInvocationWithOptions) {
+ // sharedCtor(id, options, fn)
+ fn = options;
+ options = id;
+ id = data;
+ data = null;
+ } else if (typeof data === 'function') {
+ // sharedCtor(fn)
fn = data;
data = null;
id = null;
+ options = null;
} else if (typeof id === 'function') {
+ // sharedCtor(data, fn)
+ // sharedCtor(id, fn)
fn = id;
+ options = null;
if (typeof data !== 'object') {
id = data;
@@ -150,36 +161,38 @@ module.exports = function(registry) {
}
}
- if (id && data) {
+ if (id != null && data) {
var model = new ModelCtor(data);
model.id = id;
fn(null, model);
} else if (data) {
fn(null, new ModelCtor(data));
- } else if (id) {
- ModelCtor.findById(id, function(err, model) {
+ } else if (id != null) {
+ var filter = {};
+ ModelCtor.findById(id, filter, options, function(err, model) {
if (err) {
fn(err);
} else if (model) {
fn(null, model);
} else {
- err = new Error('could not find a model with id ' + id);
+ err = new Error(g.f('could not find a model with {{id}} %s', id));
err.statusCode = 404;
err.code = 'MODEL_NOT_FOUND';
fn(err);
}
});
} else {
- fn(new Error('must specify an id or data'));
+ fn(new Error(g.f('must specify an {{id}} or {{data}}')));
}
};
var idDesc = ModelCtor.modelName + ' id';
- ModelCtor.sharedCtor.accepts = [
+ ModelCtor.sharedCtor.accepts = this._removeOptionsArgIfDisabled([
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
- description: idDesc}
+ description: idDesc},
// {arg: 'instance', type: 'object', http: {source: 'body'}}
- ];
+ {arg: 'options', type: 'object', http: createOptionsViaModelMethod},
+ ]);
ModelCtor.sharedCtor.http = [
{path: '/:id'}
@@ -187,13 +200,28 @@ module.exports = function(registry) {
ModelCtor.sharedCtor.returns = {root: true};
+ var remotingOptions = {};
+ extend(remotingOptions, options.remoting || {});
+
+ // create a sharedClass
+ var sharedClass = ModelCtor.sharedClass = new SharedClass(
+ ModelCtor.modelName,
+ ModelCtor,
+ remotingOptions
+ );
+
+ // setup a remoting type converter for this model
+ RemoteObjects.convert(typeName, function(val) {
+ return val ? new ModelCtor(val) : val;
+ });
+
// before remote hook
ModelCtor.beforeRemote = function(name, fn) {
var className = this.modelName;
this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes();
remotes.before(className + '.' + name, function(ctx, next) {
- fn(ctx, ctx.result, next);
+ return fn(ctx, ctx.result, next);
});
});
};
@@ -204,7 +232,7 @@ module.exports = function(registry) {
this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes();
remotes.after(className + '.' + name, function(ctx, next) {
- fn(ctx, ctx.result, next);
+ return fn(ctx, ctx.result, next);
});
});
};
@@ -229,6 +257,14 @@ module.exports = function(registry) {
sharedClass.resolve(function resolver(define) {
var relations = ModelCtor.relations || {};
+ var defineRaw = define;
+ define = function(name, options, fn) {
+ if (options.accepts) {
+ options = extend({}, options);
+ options.accepts = setupOptionsArgs(options.accepts);
+ }
+ defineRaw(name, options, fn);
+ };
// get the relations
for (var relationName in relations) {
@@ -357,6 +393,8 @@ module.exports = function(registry) {
return ACL.WRITE;
case 'updateOrCreate':
return ACL.WRITE;
+ case 'upsertWithWhere':
+ return ACL.WRITE;
case 'upsert':
return ACL.WRITE;
case 'exists':
@@ -417,9 +455,40 @@ module.exports = function(registry) {
if (options.isStatic === undefined) {
options.isStatic = true;
}
+
+ if (options.accepts) {
+ options = extend({}, options);
+ options.accepts = setupOptionsArgs(options.accepts);
+ }
+
this.sharedClass.defineMethod(name, options);
};
+ function setupOptionsArgs(accepts) {
+ if (!Array.isArray(accepts))
+ accepts = [accepts];
+
+ return accepts.map(function(arg) {
+ if (arg.http && arg.http === 'optionsFromRequest') {
+ // clone to preserve the input value
+ arg = extend({}, arg);
+ arg.http = createOptionsViaModelMethod;
+ }
+ return arg;
+ });
+ }
+
+ function createOptionsViaModelMethod(ctx) {
+ var EMPTY_OPTIONS = {};
+ var ModelCtor = ctx.method && ctx.method.ctor;
+ if (!ModelCtor)
+ return EMPTY_OPTIONS;
+ if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function')
+ return EMPTY_OPTIONS;
+ debug('createOptionsFromRemotingContext for %s', ctx.method.stringName);
+ return ModelCtor.createOptionsFromRemotingContext(ctx);
+ }
+
/**
* Disable remote invocation for the method with the given name.
*
@@ -430,7 +499,21 @@ module.exports = function(registry) {
*/
Model.disableRemoteMethod = function(name, isStatic) {
- this.sharedClass.disableMethod(name, isStatic || false);
+ var key = this.sharedClass.getKeyFromMethodNameAndTarget(name, isStatic);
+ this.sharedClass.disableMethodByName(key);
+ this.emit('remoteMethodDisabled', this.sharedClass, key);
+ };
+
+ /**
+ * Disable remote invocation for the method with the given name.
+ *
+ * @param {String} name The name of the method (include "prototype." if the method is defined on the prototype).
+ *
+ */
+
+ Model.disableRemoteMethodByName = function(name) {
+ this.sharedClass.disableMethodByName(name);
+ this.emit('remoteMethodDisabled', this.sharedClass, name);
};
Model.belongsToRemoting = function(relationName, relation, define) {
@@ -441,10 +524,13 @@ module.exports = function(registry) {
define('__get__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName},
- accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
accessType: 'READ',
- description: 'Fetches belongsTo relation ' + relationName + '.',
- returns: {arg: relationName, type: modelName, root: true}
+ description: format('Fetches belongsTo relation %s.', relationName),
+ returns: {arg: relationName, type: modelName, root: true},
}, fn);
};
@@ -452,7 +538,7 @@ module.exports = function(registry) {
if (ctx.result !== null) return cb();
var fk = ctx.getArgByName('fk');
- var msg = 'Unknown "' + toModelName + '" id "' + fk + '".';
+ var msg = g.f('Unknown "%s" id "%s".', toModelName, fk);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
@@ -466,8 +552,11 @@ module.exports = function(registry) {
define('__get__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName},
- accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
- description: 'Fetches hasOne relation ' + relationName + '.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Fetches hasOne relation %s.', relationName),
accessType: 'READ',
returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)}
@@ -476,8 +565,14 @@ module.exports = function(registry) {
define('__create__' + relationName, {
isStatic: false,
http: {verb: 'post', path: '/' + pathName},
- accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
- description: 'Creates a new instance in ' + relationName + ' of this model.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data', type: 'object', model: toModelName,
+ http: {source: 'body'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Creates a new instance in %s of this model.', relationName),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true}
});
@@ -485,8 +580,14 @@ module.exports = function(registry) {
define('__update__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName},
- accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
- description: 'Update ' + relationName + ' of this model.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data', type: 'object', model: toModelName,
+ http: {source: 'body'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Update %s of this model.', relationName),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true}
});
@@ -494,8 +595,11 @@ module.exports = function(registry) {
define('__destroy__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName},
- description: 'Deletes ' + relationName + ' of this model.',
- accessType: 'WRITE'
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Deletes %s of this model.', relationName),
+ accessType: 'WRITE',
});
};
@@ -507,10 +611,16 @@ module.exports = function(registry) {
define('__findById__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName + '/:fk'},
- accepts: {arg: 'fk', type: 'any',
- description: 'Foreign key for ' + relationName, required: true,
- http: {source: 'path'}},
- description: 'Find a related item by id for ' + relationName + '.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'fk', type: 'any',
+ description: format('Foreign key for %s', relationName),
+ required: true,
+ http: {source: 'path'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Find a related item by id for %s.', relationName),
accessType: 'READ',
returns: {arg: 'result', type: toModelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)}
@@ -520,10 +630,16 @@ module.exports = function(registry) {
define('__destroyById__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/:fk'},
- accepts: {arg: 'fk', type: 'any',
- description: 'Foreign key for ' + relationName, required: true,
- http: {source: 'path'}},
- description: 'Delete a related item by id for ' + relationName + '.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'fk', type: 'any',
+ description: format('Foreign key for %s', relationName),
+ required: true,
+ http: {source: 'path'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Delete a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: []
}, destroyByIdFunc);
@@ -532,13 +648,15 @@ module.exports = function(registry) {
define('__updateById__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/:fk'},
- accepts: [
+ accepts: this._removeOptionsArgIfDisabled([
{arg: 'fk', type: 'any',
- description: 'Foreign key for ' + relationName, required: true,
- http: {source: 'path'}},
- {arg: 'data', type: toModelName, http: {source: 'body'}}
- ],
- description: 'Update a related item by id for ' + relationName + '.',
+ description: format('Foreign key for %s', relationName),
+ required: true,
+ http: { source: 'path' }},
+ {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Update a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: {arg: 'result', type: toModelName, root: true}
}, updateByIdFunc);
@@ -549,17 +667,21 @@ module.exports = function(registry) {
var accepts = [];
if (relation.type === 'hasMany' && relation.modelThrough) {
// Restrict: only hasManyThrough relation can have additional properties
- accepts.push({arg: 'data', type: modelThrough.modelName, http: {source: 'body'}});
+ accepts.push({arg: 'data', type: 'object', model: modelThrough.modelName, http: {source: 'body'}});
}
var addFunc = this.prototype['__link__' + relationName];
define('__link__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/rel/:fk'},
- accepts: [{arg: 'fk', type: 'any',
- description: 'Foreign key for ' + relationName, required: true,
- http: {source: 'path'}}].concat(accepts),
- description: 'Add a related item by id for ' + relationName + '.',
+ accepts: [{ arg: 'fk', type: 'any',
+ description: format('Foreign key for %s', relationName),
+ required: true,
+ http: {source: 'path'}},
+ ].concat(accepts).concat(this._removeOptionsArgIfDisabled([
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ])),
+ description: format('Add a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: {arg: relationName, type: modelThrough.modelName, root: true}
}, addFunc);
@@ -568,10 +690,16 @@ module.exports = function(registry) {
define('__unlink__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
- accepts: {arg: 'fk', type: 'any',
- description: 'Foreign key for ' + relationName, required: true,
- http: {source: 'path'}},
- description: 'Remove the ' + relationName + ' relation to an item by id.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'fk', type: 'any',
+ description: format('Foreign key for %s', relationName),
+ required: true,
+ http: {source: 'path'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Remove the %s relation to an item by id.', relationName),
accessType: 'WRITE',
returns: []
}, removeFunc);
@@ -582,10 +710,16 @@ module.exports = function(registry) {
define('__exists__' + relationName, {
isStatic: false,
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
- accepts: {arg: 'fk', type: 'any',
- description: 'Foreign key for ' + relationName, required: true,
- http: {source: 'path'}},
- description: 'Check the existence of ' + relationName + ' relation to an item by id.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'fk', type: 'any',
+ description: format('Foreign key for %s', relationName),
+ required: true,
+ http: {source: 'path'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Check the existence of %s relation to an item by id.', relationName),
accessType: 'READ',
returns: {arg: 'exists', type: 'boolean', root: true},
rest: {
@@ -594,7 +728,7 @@ module.exports = function(registry) {
if (ctx.result === false) {
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
- var msg = 'Unknown "' + modelName + '" id "' + id + '".';
+ var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
@@ -627,8 +761,11 @@ module.exports = function(registry) {
define('__get__' + scopeName, {
isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName},
- accepts: {arg: 'filter', type: 'object'},
- description: 'Queries ' + scopeName + ' of ' + this.modelName + '.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'filter', type: 'object'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Queries %s of %s.', scopeName, this.modelName),
accessType: 'READ',
returns: {arg: scopeName, type: [toModelName], root: true}
});
@@ -636,8 +773,16 @@ module.exports = function(registry) {
define('__create__' + scopeName, {
isStatic: isStatic,
http: {verb: 'post', path: '/' + pathName},
- accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
- description: 'Creates a new instance in ' + scopeName + ' of this model.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data',
+ type: 'object',
+ model: toModelName,
+ http: {source: 'body'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Creates a new instance in %s of this model.', scopeName),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true}
});
@@ -645,21 +790,46 @@ module.exports = function(registry) {
define('__delete__' + scopeName, {
isStatic: isStatic,
http: {verb: 'delete', path: '/' + pathName},
- description: 'Deletes all ' + scopeName + ' of this model.',
- accessType: 'WRITE'
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'where', type: 'object',
+ // The "where" argument is not exposed in the REST API
+ // but we need to provide a value so that we can pass "options"
+ // as the third argument.
+ http: function(ctx) { return undefined; },
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Deletes all %s of this model.', scopeName),
+ accessType: 'WRITE',
});
define('__count__' + scopeName, {
isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName + '/count'},
- accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
- description: 'Counts ' + scopeName + ' of ' + this.modelName + '.',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'where', type: 'object',
+ description: 'Criteria to match model instances',
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ description: format('Counts %s of %s.', scopeName, this.modelName),
accessType: 'READ',
returns: {arg: 'count', type: 'number'}
});
};
+ Model._removeOptionsArgIfDisabled = function(accepts) {
+ if (this.settings.injectOptionsFromRemoteContext)
+ return accepts;
+ var lastArg = accepts[accepts.length - 1];
+ var hasOptions = lastArg.arg === 'options' && lastArg.type === 'object';
+ assert(hasOptions, 'last accepts argument is "options" arg');
+ return accepts.slice(0, -1);
+ };
+
/**
* Enabled deeply-nested queries of related models via REST API.
*
@@ -702,9 +872,9 @@ module.exports = function(registry) {
acceptArgs = [
{
arg: paramName, type: 'any', http: { source: 'path' },
- description: 'Foreign key for ' + relation.name + '.',
- required: true
- }
+ description: format('Foreign key for %s.', relation.name),
+ required: true,
+ },
];
} else {
httpPath = pathName;
@@ -732,12 +902,12 @@ module.exports = function(registry) {
var getterFn = relation.modelFrom.prototype[getterName];
if (typeof getterFn !== 'function') {
- throw new Error('Invalid remote method: `' + getterName + '`');
+ throw new Error(g.f('Invalid remote method: `%s`', getterName));
}
var nestedFn = relation.modelTo.prototype[method.name];
if (typeof nestedFn !== 'function') {
- throw new Error('Invalid remote method: `' + method.name + '`');
+ throw new Error(g.f('Invalid remote method: `%s`', method.name));
}
var opts = {};
@@ -806,8 +976,8 @@ module.exports = function(registry) {
listenerTree.before = listenerTree.before || {};
listenerTree.after = listenerTree.after || {};
- var beforeListeners = remotes.listenerTree.before[toModelName] || {};
- var afterListeners = remotes.listenerTree.after[toModelName] || {};
+ var beforeListeners = listenerTree.before[toModelName] || {};
+ var afterListeners = listenerTree.after[toModelName] || {};
sharedClass.methods().forEach(function(method) {
var delegateTo = method.rest && method.rest.delegateTo;
@@ -830,12 +1000,53 @@ module.exports = function(registry) {
});
} else {
- throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`');
+ var msg = g.f('Relation `%s` does not exist for model `%s`', relationName, this.modelName);
+ throw new Error(msg);
}
};
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
+ /**
+ * Create "options" value to use when invoking model methods
+ * via strong-remoting (e.g. REST).
+ *
+ * Example
+ *
+ * ```js
+ * MyModel.myMethod = function(options, cb) {
+ * // by default, options contains only one property "accessToken"
+ * var accessToken = options && options.accessToken;
+ * var userId = accessToken && accessToken.userId;
+ * var message = 'Hello ' + (userId ? 'user #' + userId : 'anonymous');
+ * cb(null, message);
+ * });
+ *
+ * MyModel.remoteMethod('myMethod', {
+ * accepts: {
+ * arg: 'options',
+ * type: 'object',
+ * // "optionsFromRequest" is a loopback-specific HTTP mapping that
+ * // calls Model's createOptionsFromRemotingContext
+ * // to build the argument value
+ * http: 'optionsFromRequest'
+ * },
+ * returns: {
+ * arg: 'message',
+ * type: 'string'
+ * }
+ * });
+ * ```
+ *
+ * @param {Object} ctx A strong-remoting Context instance
+ * @returns {Object} The value to pass to "options" argument.
+ */
+ Model.createOptionsFromRemotingContext = function(ctx) {
+ return {
+ accessToken: ctx.req.accessToken,
+ };
+ };
+
// setup the initial model
Model.setup();
diff --git a/lib/persisted-model.js b/lib/persisted-model.js
index b3bb62281..fd7d9db11 100644
--- a/lib/persisted-model.js
+++ b/lib/persisted-model.js
@@ -1,7 +1,13 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module Dependencies.
*/
+var g = require('strong-globalize')();
var runtime = require('./runtime');
var assert = require('assert');
var async = require('async');
@@ -60,9 +66,10 @@ module.exports = function(registry) {
function throwNotAttached(modelName, methodName) {
throw new Error(
- 'Cannot call ' + modelName + '.' + methodName + '().' +
- ' The ' + methodName + ' method has not been setup.' +
- ' The PersistedModel has not been correctly attached to a DataSource!'
+ g.f('Cannot call %s.%s().' +
+ ' The %s method has not been setup.' +
+ ' The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!',
+ modelName, methodName, methodName)
);
}
@@ -77,7 +84,7 @@ module.exports = function(registry) {
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
- var msg = 'Unknown "' + modelName + '" id "' + id + '".';
+ var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
@@ -106,22 +113,78 @@ module.exports = function(registry) {
* @param {Object} model Updated model instance.
*/
- PersistedModel.upsert = PersistedModel.updateOrCreate = function upsert(data, callback) {
+ PersistedModel.upsert = PersistedModel.updateOrCreate = PersistedModel.patchOrCreate =
+ function upsert(data, callback) {
throwNotAttached(this.modelName, 'upsert');
};
/**
- * Find one record matching the optional `where` filter. The same as `find`, but limited to one object.
- * Returns an object, not collection.
- * If not found, create the object using data provided as second argument.
- *
- * @param {Object} where Where clause, such as `{test: 'me'}`
+ * Update or insert a model instance based on the search criteria.
+ * If there is a single instance retrieved, update the retrieved model.
+ * Creates a new model if no model instances were found.
+ * Returns an error if multiple instances are found.
+ * * @param {Object} [where] `where` filter, like
+ * ```
+ * { key: val, key2: {gt: 'val2'}, ...}
+ * ```
*
see
* [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods).
+ * @param {Object} data The model instance data to insert.
+ * @callback {Function} callback Callback function called with `cb(err, obj)` signature.
+ * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
+ * @param {Object} model Updated model instance.
+ */
+
+ PersistedModel.upsertWithWhere =
+ PersistedModel.patchOrCreateWithWhere = function upsertWithWhere(where, data, callback) {
+ throwNotAttached(this.modelName, 'upsertWithWhere');
+ };
+
+ /**
+ * Replace or insert a model instance; replace existing record if one is found,
+ * such that parameter `data.id` matches `id` of model instance; otherwise,
+ * insert a new record.
+ * @param {Object} data The model instance data.
+ * @options {Object} [options] Options for replaceOrCreate
+ * @property {Boolean} validate Perform validation before saving. Default is true.
+ * @callback {Function} callback Callback function called with `cb(err, obj)` signature.
+ * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
+ * @param {Object} model Replaced model instance.
+ */
+
+ PersistedModel.replaceOrCreate = function replaceOrCreate(data, callback) {
+ throwNotAttached(this.modelName, 'replaceOrCreate');
+ };
+
+ /**
+ * Finds one record matching the optional filter object. If not found, creates
+ * the object using the data provided as second argument. In this sense it is
+ * the same as `find`, but limited to one object. Returns an object, not
+ * collection. If you don't provide the filter object argument, it tries to
+ * locate an existing object that matches the `data` argument.
+ *
+ * @options {Object} [filter] Optional Filter object; see below.
+ * @property {String|Object|Array} fields Identify fields to include in return result.
+ *
See [Fields filter](http://docs.strongloop.com/display/LB/Fields+filter).
+ * @property {String|Object|Array} include See PersistedModel.include documentation.
+ *
See [Include filter](http://docs.strongloop.com/display/LB/Include+filter).
+ * @property {Number} limit Maximum number of instances to return.
+ *
See [Limit filter](http://docs.strongloop.com/display/LB/Limit+filter).
+ * @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
+ *
See [Order filter](http://docs.strongloop.com/display/LB/Order+filter).
+ * @property {Number} skip Number of results to skip.
+ *
See [Skip filter](http://docs.strongloop.com/display/LB/Skip+filter).
+ * @property {Object} where Where clause, like
+ * ```
+ * {where: {key: val, key2: {gt: val2}, ...}}
+ * ```
+ *
See
+ * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforqueries).
* @param {Object} data Data to insert if object matching the `where` filter is not found.
- * @callback {Function} callback Callback function called with `cb(err, instance)` arguments. Required.
+ * @callback {Function} callback Callback function called with `cb(err, instance, created)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
* @param {Object} instance Model instance matching the `where` filter, if found.
+ * @param {Boolean} created True if the instance matching the `where` filter was created.
*/
PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
@@ -456,10 +519,45 @@ module.exports = function(registry) {
* @param {Object} instance Updated instance.
*/
- PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb) {
+ PersistedModel.prototype.updateAttributes = PersistedModel.prototype.patchAttributes =
+ function updateAttributes(data, cb) {
throwNotAttached(this.modelName, 'updateAttributes');
};
+ /**
+ * Replace attributes for a model instance and persist it into the datasource.
+ * Performs validation before replacing.
+ *
+ * @param {Object} data Data to replace.
+ * @options {Object} [options] Options for replace
+ * @property {Boolean} validate Perform validation before saving. Default is true.
+ * @callback {Function} callback Callback function called with `(err, instance)` arguments.
+ * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
+ * @param {Object} instance Replaced instance.
+ */
+
+ PersistedModel.prototype.replaceAttributes = function replaceAttributes(data, cb) {
+ throwNotAttached(this.modelName, 'replaceAttributes');
+ };
+
+ /**
+ * Replace attributes for a model instance whose id is the first input
+ * argument and persist it into the datasource.
+ * Performs validation before replacing.
+ *
+ * @param {*} id The ID value of model instance to replace.
+ * @param {Object} data Data to replace.
+ * @options {Object} [options] Options for replace
+ * @property {Boolean} validate Perform validation before saving. Default is true.
+ * @callback {Function} callback Callback function called with `(err, instance)` arguments.
+ * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
+ * @param {Object} instance Replaced instance.
+ */
+
+ PersistedModel.replaceById = function replaceById(id, data, cb) {
+ throwNotAttached(this.modelName, 'replaceById');
+ };
+
/**
* Reload object from persistence. Requires `id` member of `object` to be able to call `find`.
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
@@ -528,6 +626,9 @@ module.exports = function(registry) {
var typeName = PersistedModel.modelName;
var options = PersistedModel.settings;
+ // This is just for LB 2.x
+ options.replaceOnPUT = options.replaceOnPUT === true;
+
function setRemoting(scope, name, options) {
var fn = scope[name];
fn._delegate = true;
@@ -538,24 +639,82 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'create', {
description: 'Create a new instance of the model and persist it into the data source.',
accessType: 'WRITE',
- accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data', type: 'object', model: typeName, allowArray: true,
+ description: 'Model instance data',
+ http: {source: 'body'},
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'post', path: '/'}
});
- setRemoting(PersistedModel, 'upsert', {
- aliases: ['updateOrCreate'],
- description: 'Update an existing model instance or insert a new one into the data source.',
+ var upsertOptions = {
+ aliases: ['patchOrCreate', 'updateOrCreate'],
+ description: 'Patch an existing model instance or insert a new one into the data source.',
+ accessType: 'WRITE',
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
+ description: 'Model instance data',
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ returns: {arg: 'data', type: typeName, root: true},
+ http: [{verb: 'patch', path: '/'}],
+ };
+
+ if (!options.replaceOnPUT) {
+ upsertOptions.http.unshift({ verb: 'put', path: '/' });
+ }
+ setRemoting(PersistedModel, 'upsert', upsertOptions);
+
+ var replaceOrCreateOptions = {
+ description: 'Replace an existing model instance or insert a new one into the data source.',
accessType: 'WRITE',
- accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data', type: 'object', model: typeName,
+ http: {source: 'body'},
+ description: 'Model instance data',
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'data', type: typeName, root: true},
- http: {verb: 'put', path: '/'}
+ http: [{verb: 'post', path: '/replaceOrCreate'}],
+ };
+
+ if (options.replaceOnPUT) {
+ replaceOrCreateOptions.http.push({ verb: 'put', path: '/' });
+ }
+
+ setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions);
+
+ setRemoting(PersistedModel, 'upsertWithWhere', {
+ aliases: ['patchOrCreateWithWhere'],
+ description: 'Update an existing model instance or insert a new one into ' +
+ 'the data source based on the where criteria.',
+ accessType: 'WRITE',
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'where', type: 'object', http: {source: 'query'},
+ description: 'Criteria to match model instances'},
+ {arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
+ description: 'An object of model property name/value pairs'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ returns: { arg: 'data', type: typeName, root: true },
+ http: { verb: 'post', path: '/upsertWithWhere' },
});
setRemoting(PersistedModel, 'exists', {
description: 'Check whether a model instance exists in the data source.',
accessType: 'READ',
- accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'id', type: 'any', description: 'Model id', required: true},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'exists', type: 'boolean'},
http: [
{verb: 'get', path: '/:id/exists'},
@@ -584,23 +743,51 @@ module.exports = function(registry) {
});
setRemoting(PersistedModel, 'findById', {
- description: 'Find a model instance by id from the data source.',
+ description: 'Find a model instance by {{id}} from the data source.',
accessType: 'READ',
- accepts: [
+ accepts: this._removeOptionsArgIfDisabled([
{ arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}},
- { arg: 'filter', type: 'object',
- description: 'Filter defining fields and include'}
- ],
+ {arg: 'filter', type: 'object',
+ description:
+ 'Filter defining fields and include - must be a JSON-encoded string (' +
+ '{"something":"value"})'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/:id'},
rest: {after: convertNullToNotFoundError}
});
+ var replaceByIdOptions = {
+ description: 'Replace attributes for a model instance and persist it into the data source.',
+ accessType: 'WRITE',
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'id', type: 'any', description: 'Model id', required: true,
+ http: {source: 'path'}},
+ {arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description:
+ 'Model instance data'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
+ returns: { arg: 'data', type: typeName, root: true },
+ http: [{ verb: 'post', path: '/:id/replace' }],
+ };
+
+ if (options.replaceOnPUT) {
+ replaceByIdOptions.http.push({ verb: 'put', path: '/:id' });
+ }
+
+ setRemoting(PersistedModel, 'replaceById', replaceByIdOptions);
+
setRemoting(PersistedModel, 'find', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
- accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'filter', type: 'object', description:
+ 'Filter defining fields, where, include, order, offset, and limit - must be a ' +
+ 'JSON-encoded string ({"something":"value"})'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'data', type: [typeName], root: true},
http: {verb: 'get', path: '/'}
});
@@ -608,7 +795,12 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'findOne', {
description: 'Find first instance of the model matched by filter from the data source.',
accessType: 'READ',
- accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'filter', type: 'object', description:
+ 'Filter defining fields, where, include, order, offset, and limit - must be a ' +
+ 'JSON-encoded string ({"something":"value"})'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/findOne'},
rest: {after: convertNullToNotFoundError}
@@ -617,7 +809,10 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'destroyAll', {
description: 'Delete all matching records.',
accessType: 'WRITE',
- accepts: {arg: 'where', type: 'object', description: 'filter.where object'},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'where', type: 'object', description: 'filter.where object'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {
arg: 'count',
type: 'object',
@@ -630,29 +825,38 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'updateAll', {
aliases: ['update'],
- description: 'Update instances of the model matched by where from the data source.',
+ description: 'Update instances of the model matched by {{where}} from the data source.',
accessType: 'WRITE',
- accepts: [
- {arg: 'where', type: 'object', http: {source: 'query'},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'where', type: 'object', http: { source: 'query'},
description: 'Criteria to match model instances'},
- {arg: 'data', type: 'object', http: {source: 'body'},
+ {arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
description: 'An object of model property name/value pairs'},
- ],
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {
- arg: 'count',
- description: 'The number of instances updated',
- type: 'object',
- root: true
+ arg: 'info',
+ description: 'Information related to the outcome of the operation',
+ type: {
+ count: {
+ type: 'number',
+ description: 'The number of instances updated',
+ },
+ },
+ root: true,
},
http: {verb: 'post', path: '/update'}
});
setRemoting(PersistedModel, 'deleteById', {
aliases: ['destroyById', 'removeById'],
- description: 'Delete a model instance by id from the data source.',
+ description: 'Delete a model instance by {{id}} from the data source.',
accessType: 'WRITE',
- accepts: {arg: 'id', type: 'any', description: 'Model id', required: true,
- http: {source: 'path'}},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'id', type: 'any', description: 'Model id', required: true,
+ http: {source: 'path'}},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
http: {verb: 'del', path: '/:id'},
returns: {arg: 'count', type: 'object', root: true}
});
@@ -660,18 +864,35 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'count', {
description: 'Count instances of the model matched by where from the data source.',
accessType: 'READ',
- accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
+ accepts: this._removeOptionsArgIfDisabled([
+ {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'count', type: 'number'},
http: {verb: 'get', path: '/count'}
});
- setRemoting(PersistedModel.prototype, 'updateAttributes', {
- description: 'Update attributes for a model instance and persist it into the data source.',
+ var updateAttributesOptions = {
+ aliases: ['patchAttributes'],
+ description: 'Patch attributes for a model instance and persist it into the data source.',
accessType: 'WRITE',
- accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'},
+ accepts: this._removeOptionsArgIfDisabled([
+ {
+ arg: 'data', type: 'object', model: typeName,
+ http: {source: 'body'},
+ description: 'An object of model property name/value pairs',
+ },
+ {arg: 'options', type: 'object', http: 'optionsFromRequest'},
+ ]),
returns: {arg: 'data', type: typeName, root: true},
- http: {verb: 'put', path: '/'}
- });
+ http: [{verb: 'patch', path: '/'}],
+ };
+
+ setRemoting(PersistedModel.prototype, 'updateAttributes', updateAttributesOptions);
+
+ if (!options.replaceOnPUT) {
+ updateAttributesOptions.http.unshift({ verb: 'put', path: '/' });
+ }
if (options.trackChanges || options.enableRemoteReplication) {
setRemoting(PersistedModel, 'diff', {
@@ -680,7 +901,7 @@ module.exports = function(registry) {
accepts: [
{arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'},
{arg: 'remoteChanges', type: 'array', description: 'an array of change objects',
- http: {source: 'body'}}
+ http: {source: 'body'}}
],
returns: {arg: 'result', type: 'object', root: true},
http: {verb: 'post', path: '/diff'}
@@ -746,10 +967,9 @@ module.exports = function(registry) {
});
setRemoting(PersistedModel, 'updateLastChange', {
- description: [
- 'Update the properties of the most recent change record',
- 'kept for this instance.'
- ],
+ description:
+ 'Update the properties of the most recent change record ' +
+ 'kept for this instance.',
accessType: 'WRITE',
accepts: [
{
@@ -757,8 +977,8 @@ module.exports = function(registry) {
description: 'Model id'
},
{
- arg: 'data', type: 'object', http: {source: 'body'},
- description: 'An object of Change property name/value pairs'
+ arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
+ description: 'An object of Change property name/value pairs',
},
],
returns: { arg: 'result', type: this.Change.modelName, root: true },
@@ -882,12 +1102,7 @@ module.exports = function(registry) {
PersistedModel.checkpoint = function(cb) {
var Checkpoint = this.getChangeModel().getCheckpointModel();
- this.getSourceId(function(err, sourceId) {
- if (err) return cb(err);
- Checkpoint.create({
- sourceId: sourceId
- }, cb);
- });
+ Checkpoint.bumpLastSeq(cb);
};
/**
@@ -908,7 +1123,7 @@ module.exports = function(registry) {
*
* @param {Number} [since] Since this checkpoint
* @param {Model} targetModel Target this model class
- * @param {Object} [options]
+ * @param {Object} [options] An optional options object to pass to underlying data-access calls.
* @param {Object} [options.filter] Replicate models that match this filter
* @callback {Function} [callback] Callback function called with `(err, conflicts)` arguments.
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
@@ -933,6 +1148,10 @@ module.exports = function(registry) {
since = { source: since, target: since };
}
+ if (typeof options === 'function') {
+ options = {};
+ }
+
options = options || {};
var sourceModel = this;
@@ -1047,7 +1266,7 @@ module.exports = function(registry) {
function bulkUpdate(_updates, cb) {
debug('\tstarting bulk update');
updates = _updates;
- targetModel.bulkUpdate(updates, function(err) {
+ targetModel.bulkUpdate(updates, options, function(err) {
var conflicts = err && err.details && err.details.conflicts;
if (conflicts && err.statusCode == 409) {
diff.conflicts = conflicts;
@@ -1131,7 +1350,7 @@ module.exports = function(registry) {
if (err) return cb(err);
if (!inst) {
return cb &&
- cb(new Error('Missing data for change: ' + change.modelId));
+ cb(new Error(g.f('Missing data for change: %s', change.modelId)));
}
if (inst.toObject) {
update.data = inst.toObject();
@@ -1161,15 +1380,28 @@ module.exports = function(registry) {
* **Note: this is not atomic**
*
* @param {Array} updates An updates list, usually from [createUpdates()](#persistedmodel-createupdates).
+ * @param {Object} [options] An optional options object to pass to underlying data-access calls.
* @param {Function} callback Callback function.
*/
- PersistedModel.bulkUpdate = function(updates, callback) {
+ PersistedModel.bulkUpdate = function(updates, options, callback) {
var tasks = [];
var Model = this;
var Change = this.getChangeModel();
var conflicts = [];
+ var lastArg = arguments[arguments.length - 1];
+
+ if (typeof lastArg === 'function' && arguments.length > 1) {
+ callback = lastArg;
+ }
+
+ if (typeof options === 'function') {
+ options = {};
+ }
+
+ options = options || {};
+
buildLookupOfAffectedModelData(Model, updates, function(err, currentMap) {
if (err) return callback(err);
@@ -1179,18 +1411,18 @@ module.exports = function(registry) {
switch (update.type) {
case Change.UPDATE:
tasks.push(function(cb) {
- applyUpdate(Model, id, current, update.data, update.change, conflicts, cb);
+ applyUpdate(Model, id, current, update.data, update.change, conflicts, options, cb);
});
break;
case Change.CREATE:
tasks.push(function(cb) {
- applyCreate(Model, id, current, update.data, update.change, conflicts, cb);
+ applyCreate(Model, id, current, update.data, update.change, conflicts, options, cb);
});
break;
case Change.DELETE:
tasks.push(function(cb) {
- applyDelete(Model, id, current, update.change, conflicts, cb);
+ applyDelete(Model, id, current, update.change, conflicts, options, cb);
});
break;
}
@@ -1199,7 +1431,7 @@ module.exports = function(registry) {
async.parallel(tasks, function(err) {
if (err) return callback(err);
if (conflicts.length) {
- err = new Error('Conflict');
+ err = new Error(g.f('Conflict'));
err.statusCode = 409;
err.details = { conflicts: conflicts };
return callback(err);
@@ -1224,7 +1456,7 @@ module.exports = function(registry) {
});
}
- function applyUpdate(Model, id, current, data, change, conflicts, cb) {
+ function applyUpdate(Model, id, current, data, change, conflicts, options, cb) {
var Change = Model.getChangeModel();
var rev = current ? Change.revisionForInst(current) : null;
@@ -1242,7 +1474,7 @@ module.exports = function(registry) {
// but not included in `data`
// See https://github.com/strongloop/loopback/issues/1215
- Model.updateAll(current.toObject(), data, function(err, result) {
+ Model.updateAll(current.toObject(), data, options, function(err, result) {
if (err) return cb(err);
var count = result && result.count;
@@ -1263,22 +1495,22 @@ module.exports = function(registry) {
case undefined:
case null:
return cb(new Error(
- 'Cannot apply bulk updates, ' +
+ g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' +
- 'the number of updated records.'));
+ 'the number of updated records.')));
default:
debug('%s.updateAll modified unexpected number of instances: %j',
Model.modelName, count);
return cb(new Error(
- 'Bulk update failed, the connector has modified unexpected ' +
- 'number of records: ' + JSON.stringify(count)));
+ g.f('Bulk update failed, the connector has modified unexpected ' +
+ 'number of records: %s', JSON.stringify(count))));
}
});
}
- function applyCreate(Model, id, current, data, change, conflicts, cb) {
- Model.create(data, function(createErr) {
+ function applyCreate(Model, id, current, data, change, conflicts, options, cb) {
+ Model.create(data, options, function(createErr) {
if (!createErr) return cb();
// We don't have a reliable way how to detect the situation
@@ -1306,7 +1538,7 @@ module.exports = function(registry) {
}
}
- function applyDelete(Model, id, current, change, conflicts, cb) {
+ function applyDelete(Model, id, current, change, conflicts, options, cb) {
if (!current) {
// The instance was either already deleted or not created at all,
// we are done.
@@ -1324,7 +1556,7 @@ module.exports = function(registry) {
return Change.rectifyModelChanges(Model.modelName, [id], cb);
}
- Model.deleteAll(current.toObject(), function(err, result) {
+ Model.deleteAll(current.toObject(), options, function(err, result) {
if (err) return cb(err);
var count = result && result.count;
@@ -1345,16 +1577,16 @@ module.exports = function(registry) {
case undefined:
case null:
return cb(new Error(
- 'Cannot apply bulk updates, ' +
+ g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' +
- 'the number of deleted records.'));
+ 'the number of deleted records.')));
default:
debug('%s.deleteAll modified unexpected number of instances: %j',
Model.modelName, count);
return cb(new Error(
- 'Bulk update failed, the connector has deleted unexpected ' +
- 'number of records: ' + JSON.stringify(count)));
+ g.f('Bulk update failed, the connector has deleted unexpected ' +
+ 'number of records: %s', JSON.stringify(count))));
}
});
}
@@ -1413,14 +1645,16 @@ module.exports = function(registry) {
var idDefn = idProp && idProp.defaultFn;
if (idType !== String || !(idDefn === 'uuid' || idDefn === 'guid')) {
deprecated('The model ' + this.modelName + ' is tracking changes, ' +
- 'which requries a string id with GUID/UUID default value.');
+ 'which requires a string id with GUID/UUID default value.');
}
Model.observe('after save', rectifyOnSave);
Model.observe('after delete', rectifyOnDelete);
- if (runtime.isServer) {
+ // Only run if the run time is server
+ // Can switch off cleanup by setting the interval to -1
+ if (runtime.isServer && cleanupInterval > 0) {
// initial cleanup
cleanup();
@@ -1449,7 +1683,7 @@ module.exports = function(registry) {
ctx.instance, ctx.currentInstance, ctx.where, ctx.data);
}
- if (id) {
+ if (id != null) {
ctx.Model.rectifyChange(id, reportErrorAndNext);
} else {
ctx.Model.rectifyAllChanges(reportErrorAndNext);
@@ -1473,7 +1707,7 @@ module.exports = function(registry) {
debug('context instance:%j where:%j', ctx.instance, ctx.where);
}
- if (id) {
+ if (id != null) {
ctx.Model.rectifyChange(id, reportErrorAndNext);
} else {
ctx.Model.rectifyAllChanges(reportErrorAndNext);
@@ -1569,8 +1803,8 @@ module.exports = function(registry) {
this.findLastChange(id, function(err, inst) {
if (err) return cb(err);
if (!inst) {
- err = new Error('No change record found for ' +
- self.modelName + ' with id ' + id);
+ err = new Error(g.f('No change record found for %s with id %s',
+ self.modelName, id));
err.statusCode = 404;
return cb(err);
}
diff --git a/lib/registry.js b/lib/registry.js
index ade0e2e88..f114e37c0 100644
--- a/lib/registry.js
+++ b/lib/registry.js
@@ -1,3 +1,9 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
var assert = require('assert');
var extend = require('util')._extend;
var juggler = require('loopback-datasource-juggler');
@@ -108,11 +114,11 @@ Registry.prototype.createModel = function(name, properties, options) {
if (BaseModel === undefined) {
if (baseName === 'DataModel') {
- console.warn('Model `%s` is extending deprecated `DataModel. ' +
+ g.warn('Model `%s` is extending deprecated `DataModel. ' +
'Use `PersistedModel` instead.', name);
BaseModel = this.getModel('PersistedModel');
} else {
- console.warn('Model `%s` is extending an unknown model `%s`. ' +
+ g.warn('Model `%s` is extending an unknown model `%s`. ' +
'Using `PersistedModel` as the base.', name, baseName);
}
}
@@ -192,7 +198,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
relations[key] = extend(relations[key] || {}, config.relations[key]);
});
} else if (config.relations != null) {
- console.warn('The relations property of `%s` configuration ' +
+ g.warn('The relations property of `%s` configuration ' +
'must be an object', modelName);
}
@@ -203,7 +209,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
addACL(acls, acl);
});
} else if (config.acls != null) {
- console.warn('The acls property of `%s` configuration ' +
+ g.warn('The acls property of `%s` configuration ' +
'must be an array of objects', modelName);
}
@@ -220,12 +226,12 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
if (!(p in excludedProperties)) {
settings[p] = config.options[p];
} else {
- console.warn('Property `%s` cannot be reconfigured for `%s`',
+ g.warn('Property `%s` cannot be reconfigured for `%s`',
p, modelName);
}
}
} else if (config.options != null) {
- console.warn('The options property of `%s` configuration ' +
+ g.warn('The options property of `%s` configuration ' +
'must be an object', modelName);
}
@@ -244,8 +250,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
} else {
debug('Model `%s` is not attached to any DataSource, possibly by a mistake.',
modelName);
- console.warn(
- 'The configuration of `%s` is missing `dataSource` property.\n' +
+ g.warn(
+ 'The configuration of `%s` is missing {{`dataSource`}} property.\n' +
'Use `null` or `false` to mark models not attached to any data source.',
modelName);
}
@@ -257,7 +263,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
if (!methods) return;
if (typeof methods !== 'object') {
- console.warn('Ignoring non-object "methods" setting of "%s".',
+ g.warn('Ignoring non-object "methods" setting of "%s".',
ModelCtor.modelName);
return;
}
@@ -265,11 +271,11 @@ Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
Object.keys(methods).forEach(function(key) {
var meta = methods[key];
if (typeof meta.isStatic !== 'boolean') {
- console.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' +
+ g.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' +
'flag, the method is registered as an instance method.',
ModelCtor.modelName,
key);
- console.warn('This behaviour may change in the next major version.');
+ g.warn('This behaviour may change in the next major version.');
}
ModelCtor.remoteMethod(key, meta);
});
@@ -301,7 +307,7 @@ Registry.prototype.getModel = function(modelName) {
var model = this.findModel(modelName);
if (model) return model;
- throw new Error('Model not found: ' + modelName);
+ throw new Error(g.f('Model not found: %s', modelName));
};
/**
diff --git a/lib/runtime.js b/lib/runtime.js
index 7e791f5b1..8799447bb 100644
--- a/lib/runtime.js
+++ b/lib/runtime.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*
* This is an internal file that should not be used outside of loopback.
* All exported entities can be accessed via the `loopback` object.
diff --git a/lib/server-app.js b/lib/server-app.js
index 237a62540..f6b7b1257 100644
--- a/lib/server-app.js
+++ b/lib/server-app.js
@@ -1,3 +1,10 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var g = require('strong-globalize')();
+
var assert = require('assert');
var express = require('express');
var merge = require('util')._extend;
@@ -183,7 +190,7 @@ proto.middleware = function(name, paths, handler) {
}
if (this._requestHandlingPhases.indexOf(name) === -1)
- throw new Error('Unknown middleware phase ' + name);
+ throw new Error(g.f('Unknown {{middleware}} phase %s', name));
debug('use %s %s %s', fullPhaseName, paths, handlerName);
diff --git a/lib/utils.js b/lib/utils.js
index 306a1764a..555a18616 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2015,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
exports.createPromiseCallback = createPromiseCallback;
function createPromiseCallback() {
diff --git a/package.json b/package.json
index bccf39157..941c37598 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,9 @@
{
"name": "loopback",
- "version": "2.26.2",
+ "version": "2.42.0",
+ "publishConfig": {
+ "tag": "lts"
+ },
"description": "LoopBack: Open Source Framework for Node.js",
"homepage": "http://loopback.io",
"keywords": [
@@ -29,64 +32,78 @@
"mBaaS"
],
"scripts": {
- "test": "grunt mocha-and-karma"
+ "coverage": "nyc report --reporter=text-lcov | coveralls",
+ "test": "nyc grunt mocha-and-karma"
+ },
+ "engines": {
+ "node": ">=4.0.0"
},
"dependencies": {
- "async": "^0.9.0",
+ "async": "^2.0.1",
"bcryptjs": "^2.1.0",
"body-parser": "^1.12.0",
"canonical-json": "0.0.4",
- "continuation-local-storage": "^3.1.3",
"cookie-parser": "^1.3.4",
"debug": "^2.1.2",
"depd": "^1.0.0",
"ejs": "^2.3.1",
"errorhandler": "^1.3.4",
- "express": "^4.12.2",
+ "express": "^4.16.2",
"inflection": "^1.6.0",
+ "isemail": "^1.2.0",
"loopback-connector-remote": "^1.0.3",
+ "loopback-context": "^1.0.0",
"loopback-phase": "^1.2.0",
- "nodemailer": "^1.3.1",
- "nodemailer-stub-transport": "^0.1.5",
+ "nodemailer": "^2.5.0",
+ "nodemailer-stub-transport": "^1.0.0",
"serve-favicon": "^2.2.0",
"stable": "^0.1.5",
+ "strong-globalize": "^2.6.2",
"strong-remoting": "^2.21.0",
"uid2": "0.0.3",
"underscore.string": "^3.0.3"
},
"peerDependencies": {
- "loopback-datasource-juggler": "^2.19.0"
+ "loopback-datasource-juggler": "^2.56.0"
},
"devDependencies": {
- "bluebird": "^2.9.9",
- "browserify": "^10.0.0",
- "chai": "^2.1.1",
+ "babel-preset-es2015": "^6.24.1",
+ "babelify": "^7.3.0",
+ "bluebird": "^3.4.1",
+ "browserify": "^13.1.0",
+ "chai": "^3.5.0",
+ "coveralls": "^2.11.15",
"es5-shim": "^4.1.0",
- "grunt": "^0.4.5",
- "grunt-browserify": "^3.5.0",
- "grunt-cli": "^0.1.13",
- "grunt-contrib-jshint": "^0.11.0",
- "grunt-contrib-uglify": "^0.9.1",
- "grunt-contrib-watch": "^0.6.1",
- "grunt-jscs": "^1.5.0",
- "grunt-karma": "^0.10.1",
+ "express-session": "^1.14.0",
+ "grunt": "^1.0.1",
+ "grunt-browserify": "^5.0.0",
+ "grunt-cli": "^1.2.0",
+ "grunt-contrib-jshint": "^1.0.0",
+ "grunt-contrib-uglify": "^2.0.0",
+ "grunt-contrib-watch": "^1.0.0",
+ "grunt-jscs": "^3.0.1",
+ "grunt-karma": "^2.0.0",
"grunt-mocha-test": "^0.12.7",
- "karma": "^0.12.31",
- "karma-browserify": "^4.0.0",
- "karma-chrome-launcher": "^0.1.7",
- "karma-firefox-launcher": "^0.1.4",
- "karma-html2js-preprocessor": "^0.1.0",
- "karma-junit-reporter": "^0.2.2",
- "karma-mocha": "^0.1.10",
- "karma-phantomjs-launcher": "^0.1.4",
- "karma-script-launcher": "^0.1.0",
+ "karma": "^1.1.2",
+ "karma-browserify": "^5.0.5",
+ "karma-chrome-launcher": "^1.0.1",
+ "karma-firefox-launcher": "^1.0.0",
+ "karma-html2js-preprocessor": "^1.0.0",
+ "karma-junit-reporter": "^1.0.0",
+ "karma-mocha": "^1.1.1",
+ "karma-phantomjs-launcher": "^1.0.0",
+ "karma-script-launcher": "^1.0.0",
"loopback-boot": "^2.7.0",
- "loopback-datasource-juggler": "^2.19.1",
- "loopback-testing": "~1.1.0",
- "mocha": "^2.1.0",
+ "loopback-datasource-juggler": "^2.56.0",
+ "loopback-testing": "^1.4.0",
+ "mocha": "^3.0.0",
+ "nyc": "^10.1.2",
+ "phantomjs-prebuilt": "^2.1.7",
"sinon": "^1.13.0",
+ "sinon-chai": "^2.8.0",
"strong-task-emitter": "^0.0.6",
- "supertest": "^0.15.0"
+ "supertest": "^2.0.0",
+ "supertest-as-promised": "^4.0.2"
},
"repository": {
"type": "git",
@@ -95,15 +112,22 @@
"browser": {
"express": "./lib/browser-express.js",
"./lib/server-app.js": "./lib/browser-express.js",
- "./server/current-context.js": "./browser/current-context.js",
"connect": false,
"nodemailer": false,
"supertest": false,
"depd": "loopback-datasource-juggler/lib/browser.depd.js",
"bcrypt": false
},
- "license": "MIT",
- "optionalDependencies": {
- "sl-blip": "http://blip.strongloop.com/loopback@2.26.2"
- }
+ "config": {
+ "ci": {
+ "debug": "*,-mocha:*,-eslint:*"
+ }
+ },
+ "ci": {
+ "downstreamIgnoreList": [
+ "dashboard-controller",
+ "gateway-director-management-interface"
+ ]
+ },
+ "license": "MIT"
}
diff --git a/server/current-context.js b/server/current-context.js
deleted file mode 100644
index 6b8304e22..000000000
--- a/server/current-context.js
+++ /dev/null
@@ -1,138 +0,0 @@
-var juggler = require('loopback-datasource-juggler');
-var remoting = require('strong-remoting');
-var cls = require('continuation-local-storage');
-var domain = require('domain');
-
-module.exports = function(loopback) {
-
- /**
- * Get the current context object. The context is preserved
- * across async calls, it behaves like a thread-local storage.
- *
- * @returns {ChainedContext} The context object or null.
- */
- loopback.getCurrentContext = function() {
- // A placeholder method, see loopback.createContext() for the real version
- return null;
- };
-
- /**
- * Run the given function in such way that
- * `loopback.getCurrentContext` returns the
- * provided context object.
- *
- * **NOTE**
- *
- * The method is supported on the server only, it does not work
- * in the browser at the moment.
- *
- * @param {Function} fn The function to run, it will receive arguments
- * (currentContext, currentDomain).
- * @param {ChainedContext} context An optional context object.
- * When no value is provided, then the default global context is used.
- */
- loopback.runInContext = function(fn, context) {
- var currentDomain = domain.create();
- currentDomain.oldBind = currentDomain.bind;
- currentDomain.bind = function(callback, context) {
- return currentDomain.oldBind(ns.bind(callback, context), context);
- };
-
- var ns = context || loopback.createContext('loopback');
-
- currentDomain.run(function() {
- ns.run(function executeInContext(context) {
- fn(ns, currentDomain);
- });
- });
- };
-
- /**
- * Create a new LoopBackContext instance that can be used
- * for `loopback.runInContext`.
- *
- * **NOTES**
- *
- * At the moment, `loopback.getCurrentContext` supports
- * a single global context instance only. If you call `createContext()`
- * multiple times, `getCurrentContext` will return the last context
- * created.
- *
- * The method is supported on the server only, it does not work
- * in the browser at the moment.
- *
- * @param {String} scopeName An optional scope name.
- * @return {ChainedContext} The new context object.
- */
- loopback.createContext = function(scopeName) {
- // Make the namespace globally visible via the process.context property
- process.context = process.context || {};
- var ns = process.context[scopeName];
- if (!ns) {
- ns = cls.createNamespace(scopeName);
- process.context[scopeName] = ns;
- // Set up loopback.getCurrentContext()
- loopback.getCurrentContext = function() {
- return ns && ns.active ? ns : null;
- };
-
- chain(juggler);
- chain(remoting);
- }
- return ns;
- };
-
- /**
- * Create a chained context
- * @param {Object} child The child context
- * @param {Object} parent The parent context
- * @private
- * @constructor
- */
- function ChainedContext(child, parent) {
- this.child = child;
- this.parent = parent;
- }
-
- /**
- * Get the value by name from the context. If it doesn't exist in the child
- * context, try the parent one
- * @param {String} name Name of the context property
- * @returns {*} Value of the context property
- * @private
- */
- ChainedContext.prototype.get = function(name) {
- var val = this.child && this.child.get(name);
- if (val === undefined) {
- return this.parent && this.parent.get(name);
- }
- };
-
- ChainedContext.prototype.set = function(name, val) {
- if (this.child) {
- return this.child.set(name, val);
- } else {
- return this.parent && this.parent.set(name, val);
- }
- };
-
- ChainedContext.prototype.reset = function(name, val) {
- if (this.child) {
- return this.child.reset(name, val);
- } else {
- return this.parent && this.parent.reset(name, val);
- }
- };
-
- function chain(child) {
- if (typeof child.getCurrentContext === 'function') {
- var childContext = new ChainedContext(child.getCurrentContext(),
- loopback.getCurrentContext());
- child.getCurrentContext = function() {
- return childContext;
- };
- } else {
- child.getCurrentContext = loopback.getCurrentContext;
- }
- }
-};
diff --git a/server/middleware/context.js b/server/middleware/context.js
index 95352018f..4c5fe7b0b 100644
--- a/server/middleware/context.js
+++ b/server/middleware/context.js
@@ -1,52 +1,15 @@
-var loopback = require('../../lib/loopback');
-
-module.exports = context;
-
-var name = 'loopback';
-
-/**
- * Context middleware.
- * ```js
- * var app = loopback();
- * app.use(loopback.context(options);
- * app.use(loopback.rest());
- * app.listen();
- * ```
- * @options {Object} [options] Options for context
- * @property {String} name Context scope name.
- * @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false.
- * @header loopback.context([options])
- */
-
-function context(options) {
- options = options || {};
- var scope = options.name || name;
- var enableHttpContext = options.enableHttpContext || false;
- var ns = loopback.createContext(scope);
-
- // Return the middleware
- return function contextHandler(req, res, next) {
- if (req.loopbackContext) {
- return next();
- }
-
- loopback.runInContext(function processRequestInContext(ns, domain) {
- req.loopbackContext = ns;
-
- // Bind req/res event emitters to the given namespace
- ns.bindEmitter(req);
- ns.bindEmitter(res);
-
- // Add req/res event emitters to the current domain
- domain.add(req);
- domain.add(res);
-
- // Run the code in the context of the namespace
- if (enableHttpContext) {
- // Set up the transport context
- ns.set('http', {req: req, res: res});
- }
- next();
- });
- };
-}
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var deprecated = require('depd')('loopback');
+var g = require('strong-globalize')();
+var perRequestContext = require('loopback-context').perRequest;
+
+module.exports = function() {
+ deprecated(g.f('%s middleware is deprecated. See %s for more details.',
+ 'loopback#context',
+ 'https://docs.strongloop.com/display/APIC/Using%20current%20context'));
+ return perRequestContext.apply(this, arguments);
+};
diff --git a/server/middleware/error-handler.js b/server/middleware/error-handler.js
index c549944bf..1d30ae289 100644
--- a/server/middleware/error-handler.js
+++ b/server/middleware/error-handler.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2015,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var expressErrorHandler = require('errorhandler');
expressErrorHandler.title = 'Loopback';
diff --git a/server/middleware/favicon.js b/server/middleware/favicon.js
index d2e1fa40d..48ffac4ff 100644
--- a/server/middleware/favicon.js
+++ b/server/middleware/favicon.js
@@ -1,5 +1,16 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var favicon = require('serve-favicon');
+var path = require('path');
+
/**
* Serve the LoopBack favicon.
* @header loopback.favicon()
*/
-module.exports = require('../../lib/express-middleware').favicon;
+module.exports = function(icon, options) {
+ icon = icon || path.join(__dirname, '../../favicon.ico');
+ return favicon(icon, options);
+};
diff --git a/server/middleware/rest.js b/server/middleware/rest.js
index 9c7e23a28..f1eb3ecec 100644
--- a/server/middleware/rest.js
+++ b/server/middleware/rest.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module dependencies.
*/
@@ -68,8 +73,21 @@ function rest() {
if (handlers.length === 1) {
return handlers[0](req, res, next);
}
- async.eachSeries(handlers, function(handler, done) {
- handler(req, res, done);
- }, next);
+
+ executeHandlers(handlers, req, res, next);
};
}
+
+// A trimmed-down version of async.series that preserves current CLS context
+function executeHandlers(handlers, req, res, cb) {
+ var ix = -1;
+ next();
+
+ function next(err) {
+ if (err || ++ix >= handlers.length) {
+ cb(err);
+ } else {
+ handlers[ix](req, res, next);
+ }
+ }
+}
diff --git a/server/middleware/static.js b/server/middleware/static.js
index c01a538df..6f253dfa5 100644
--- a/server/middleware/static.js
+++ b/server/middleware/static.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/**
* Serve static assets of a LoopBack application.
*
diff --git a/server/middleware/status.js b/server/middleware/status.js
index 3e9308115..f064a9d83 100644
--- a/server/middleware/status.js
+++ b/server/middleware/status.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Export the middleware.
*/
diff --git a/server/middleware/token.js b/server/middleware/token.js
index e80eb560b..58ca056fc 100644
--- a/server/middleware/token.js
+++ b/server/middleware/token.js
@@ -1,7 +1,14 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Module dependencies.
*/
+'use strict';
+var g = require('strong-globalize')();
var loopback = require('../../lib/loopback');
var assert = require('assert');
var debug = require('debug')('loopback:middleware:token');
@@ -15,18 +22,33 @@ module.exports = token;
/*
* Rewrite the url to replace current user literal with the logged in user id
*/
-function rewriteUserLiteral(req, currentUserLiteral) {
- if (req.accessToken && req.accessToken.userId && currentUserLiteral) {
+function rewriteUserLiteral(req, currentUserLiteral, next) {
+ if (!currentUserLiteral) return next();
+ var literalRegExp = new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g');
+
+ if (req.accessToken && req.accessToken.userId) {
// Replace /me/ with /current-user-id/
var urlBeforeRewrite = req.url;
- req.url = req.url.replace(
- new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'),
+ req.url = req.url.replace(literalRegExp,
'/' + req.accessToken.userId + '$1');
+
if (req.url !== urlBeforeRewrite) {
debug('req.url has been rewritten from %s to %s', urlBeforeRewrite,
req.url);
}
+ } else if (!req.accessToken && literalRegExp.test(req.url)) {
+ debug(
+ 'URL %s matches current-user literal %s,' +
+ ' but no (valid) access token was provided.',
+ req.url, currentUserLiteral);
+
+ var e = new Error(g.f('Authorization Required'));
+ e.status = e.statusCode = 401;
+ e.code = 'AUTHORIZATION_REQUIRED';
+ return next(e);
}
+
+ next();
}
function escapeRegExp(str) {
@@ -62,6 +84,8 @@ function escapeRegExp(str) {
* @property {Array} [headers] Array of header names.
* @property {Array} [params] Array of param names.
* @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request
+ * @property {Boolean} [enableDoublecheck] Execute middleware although an instance mounted earlier in the chain didn't find a token
+ * @property {Boolean} [overwriteExistingToken] only has effect in combination with `enableDoublecheck`. If truthy, will allow to overwrite an existing accessToken.
* @property {Function|String} [model] AccessToken model name or class to use.
* @property {String} [currentUserLiteral] String literal for the current user.
* @header loopback.token([options])
@@ -80,6 +104,9 @@ function token(options) {
currentUserLiteral = escapeRegExp(currentUserLiteral);
}
+ var enableDoublecheck = !!options.enableDoublecheck;
+ var overwriteExistingToken = !!options.overwriteExistingToken;
+
return function(req, res, next) {
var app = req.app;
var registry = app.registry;
@@ -97,15 +124,27 @@ function token(options) {
'loopback.token() middleware requires a AccessToken model');
if (req.accessToken !== undefined) {
- rewriteUserLiteral(req, currentUserLiteral);
- return next();
+ if (!enableDoublecheck) {
+ // req.accessToken is defined already (might also be "null" or "false") and enableDoublecheck
+ // has not been set --> skip searching for credentials
+ return rewriteUserLiteral(req, currentUserLiteral, next);
+ }
+ if (req.accessToken && req.accessToken.id && !overwriteExistingToken) {
+ // req.accessToken.id is defined, which means that some other middleware has identified a valid user.
+ // when overwriteExistingToken is not set to a truthy value, skip searching for credentials.
+ return rewriteUserLiteral(req, currentUserLiteral, next);
+ }
+ // continue normal operation (as if req.accessToken was undefined)
}
+
TokenModel.findForRequest(req, options, function(err, token) {
req.accessToken = token || null;
- rewriteUserLiteral(req, currentUserLiteral);
- var ctx = loopback.getCurrentContext();
- if (ctx) ctx.set('accessToken', token);
- next(err);
+
+ var ctx = req.loopbackContext;
+ if (ctx && ctx.active) ctx.set('accessToken', token);
+
+ if (err) return next(err);
+ rewriteUserLiteral(req, currentUserLiteral, next);
});
};
}
diff --git a/server/middleware/url-not-found.js b/server/middleware/url-not-found.js
index dd696d79f..d204f1575 100644
--- a/server/middleware/url-not-found.js
+++ b/server/middleware/url-not-found.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*!
* Export the middleware.
* See discussion in Connect pull request #954 for more details
diff --git a/test/access-control.integration.js b/test/access-control.integration.js
index 3125a6535..23bcbf5c3 100644
--- a/test/access-control.integration.js
+++ b/test/access-control.integration.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*jshint -W030 */
var loopback = require('../');
@@ -11,8 +16,15 @@ var CURRENT_USER = {email: 'current@test.test', password: 'test'};
var debug = require('debug')('loopback:test:access-control.integration');
describe('access control - integration', function() {
+ before(function(done) {
+ if (app.booting) {
+ return app.once('booted', done);
+ }
+ done();
+ });
lt.beforeEach.withApp(app);
+ lt.beforeEach.withUserModel('user');
/*
describe('accessToken', function() {
@@ -94,7 +106,7 @@ describe('access control - integration', function() {
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
beforeEach(function() {
- this.url = '/api/users/' + this.user.id + '?ok';
+ this.url = '/api/users/' + this.loggedInAccessToken.userId + '?ok';
});
lt.describe.whenCalledRemotely('DELETE', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
@@ -110,11 +122,21 @@ describe('access control - integration', function() {
assert.equal(user.password, undefined);
});
});
+
+ // user has replaceOnPUT = false; so then both PUT and PATCH should be allowed for update
lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
});
+
+ lt.describe.whenCalledRemotely('PATCH', '/api/users/:id', function() {
+ lt.it.shouldBeAllowed();
+ });
});
+ lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/users/upsertWithWhere');
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/users/upsertWithWhere');
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/upsertWithWhere');
+
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser);
@@ -126,6 +148,7 @@ describe('access control - integration', function() {
var userCounter;
function newUserData() {
userCounter = userCounter ? ++userCounter : 1;
+
return {
email: 'new-' + userCounter + '@test.test',
password: 'test'
@@ -134,6 +157,34 @@ describe('access control - integration', function() {
});
describe('/banks', function() {
+ var SPECIAL_USER = { email: 'special@test.test', password: 'test' };
+
+ // define dynamic role that would only grant access when the authenticated user's email is equal to
+ // SPECIAL_USER's email
+
+ before(function() {
+ var roleModel = app.registry.getModel('Role');
+ var userModel = app.registry.getModel('user');
+
+ roleModel.registerResolver('$dynamic-role', function(role, context, callback) {
+ if (!(context && context.accessToken && context.accessToken.userId)) {
+ return process.nextTick(function() {
+ callback && callback(null, false);
+ });
+ }
+ var accessToken = context.accessToken;
+ userModel.findById(accessToken.userId, function(err, user) {
+ if (err) {
+ return callback(err, false);
+ }
+ if (user && user.email === SPECIAL_USER.email) {
+ return callback(null, true);
+ }
+ return callback(null, false);
+ });
+ });
+ });
+
lt.beforeEach.givenModel('bank');
lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks');
@@ -155,13 +206,18 @@ describe('access control - integration', function() {
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank);
+ lt.it.shouldBeAllowedWhenCalledByUser(SPECIAL_USER, 'DELETE', urlForBank);
+
+ lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/banks/upsertWithWhere');
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/banks/upsertWithWhere');
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/banks/upsertWithWhere');
function urlForBank() {
return '/api/banks/' + this.bank.id;
}
});
- describe('/accounts', function() {
+ describe('/accounts with replaceOnPUT true', function() {
var count = 0;
before(function() {
var roleModel = loopback.getModelByType(loopback.Role);
@@ -175,47 +231,135 @@ describe('access control - integration', function() {
});
});
- lt.beforeEach.givenModel('account');
+ lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue');
- lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts');
- lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts');
- lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts');
+ lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing');
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing');
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
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');
+ lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing');
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing');
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts-replacing');
+
+ lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST);
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST);
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST);
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount);
+
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
+ var actId;
beforeEach(function(done) {
var self = this;
-
// Create an account under the given user
- app.models.account.create({
- userId: self.user.id,
+ app.models.accountWithReplaceOnPUTtrue.create({
+ userId: self.loggedInAccessToken.userId,
balance: 100
}, function(err, act) {
- self.url = '/api/accounts/' + act.id;
+ actId = act.id;
+ self.url = '/api/accounts-replacing/' + actId;
+ done();
+ });
+ });
+
+ lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() {
+ lt.it.shouldBeAllowed();
+ });
+ lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() {
+ lt.it.shouldBeAllowed();
+ });
+ lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() {
+ lt.it.shouldBeAllowed();
+ });
+ lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() {
+ lt.it.shouldBeDenied();
+ });
+ describe('replace on POST verb', function() {
+ beforeEach(function(done) {
+ this.url = '/api/accounts-replacing/' + actId + '/replace';
+ done();
+ });
+ lt.describe.whenCalledRemotely('POST', '/api/accounts-replacing/:id/replace', function() {
+ lt.it.shouldBeAllowed();
+ });
+ });
+ });
+
+ lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
+
+ function urlForAccount() {
+ return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id;
+ }
+ function urlForReplaceAccountPOST() {
+ return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id + '/replace';
+ }
+ });
+
+ describe('/accounts with replaceOnPUT false', function() {
+ lt.beforeEach.givenModel('accountWithReplaceOnPUTfalse');
+ lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST);
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST);
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST);
+
+ lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
+
+ lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount);
+ lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount);
+
+ lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
+ var actId;
+ beforeEach(function(done) {
+ var self = this;
+ // Create an account under the given user
+ app.models.accountWithReplaceOnPUTfalse.create({
+ userId: self.loggedInAccessToken.userId,
+ balance: 100,
+ }, function(err, act) {
+ actId = act.id;
+ self.url = '/api/accounts-updating/' + actId;
done();
});
+ });
+ lt.describe.whenCalledRemotely('PATCH', '/api/accounts-updating/:id', function() {
+ lt.it.shouldBeAllowed();
});
- lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
+
+ lt.describe.whenCalledRemotely('PUT', '/api/accounts-updating/:id', function() {
+
lt.it.shouldBeAllowed();
});
- lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() {
+ lt.describe.whenCalledRemotely('GET', '/api/accounts-updating/:id', function() {
lt.it.shouldBeAllowed();
});
- lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
+ lt.describe.whenCalledRemotely('DELETE', '/api/accounts-updating/:id', function() {
lt.it.shouldBeDenied();
});
+
+ describe('replace on POST verb', function() {
+ beforeEach(function(done) {
+ this.url = '/api/accounts-updating/' + actId + '/replace';
+ done();
+ });
+ lt.describe.whenCalledRemotely('POST', '/api/accounts-updating/:id/replace', function() {
+ lt.it.shouldBeAllowed();
+ });
+ });
});
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
@@ -223,7 +367,10 @@ describe('access control - integration', function() {
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
function urlForAccount() {
- return '/api/accounts/' + this.account.id;
+ return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id;
+ }
+ function urlForReplaceAccountPOST() {
+ return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace';
}
});
diff --git a/test/access-token.test.js b/test/access-token.test.js
index 9e57cba2a..d12f17abe 100644
--- a/test/access-token.test.js
+++ b/test/access-token.test.js
@@ -1,5 +1,13 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+var cookieParser = require('cookie-parser');
var loopback = require('../');
var extend = require('util')._extend;
+var session = require('express-session');
+
var Token = loopback.AccessToken.extend('MyToken');
var ds = loopback.createDataSource({connector: loopback.Memory});
Token.attachTo(ds);
@@ -65,7 +73,7 @@ describe('loopback.token(options)', function() {
.end(done);
});
- describe('populating req.toen from HTTP Basic Auth formatted authorization header', function() {
+ describe('populating req.token from HTTP Basic Auth formatted authorization header', function() {
it('parses "standalone-token"', function(done) {
var token = this.token.id;
token = 'Basic ' + new Buffer(token).toString('base64');
@@ -144,7 +152,8 @@ describe('loopback.token(options)', function() {
.set('authorization', id)
.end(function(err, res) {
assert(!err);
- assert.deepEqual(res.body, {userId: userId});
+ assert.deepEqual(res.body, { userId: userId });
+
done();
});
});
@@ -159,7 +168,8 @@ describe('loopback.token(options)', function() {
.set('authorization', id)
.end(function(err, res) {
assert(!err);
- assert.deepEqual(res.body, {userId: userId, state: 1});
+ assert.deepEqual(res.body, { userId: userId, state: 1 });
+
done();
});
});
@@ -174,15 +184,47 @@ describe('loopback.token(options)', function() {
.set('authorization', id)
.end(function(err, res) {
assert(!err);
- assert.deepEqual(res.body, {userId: userId, state: 1});
+ assert.deepEqual(res.body, { userId: userId, state: 1 });
+
done();
});
});
+ it('should generate a 401 on a current user literal route without an authToken',
+ function(done) {
+ var app = createTestApp(null, done);
+ request(app)
+ .get('/users/me')
+ .set('authorization', null)
+ .expect(401)
+ .end(done);
+ });
+
+ it('should generate a 401 on a current user literal route with empty authToken',
+ function(done) {
+ var app = createTestApp(null, done);
+ request(app)
+ .get('/users/me')
+ .set('authorization', '')
+ .expect(401)
+ .end(done);
+ });
+
+ it('should generate a 401 on a current user literal route with invalid authToken',
+ function(done) {
+ var app = createTestApp(this.token, done);
+ request(app)
+ .get('/users/me')
+ .set('Authorization', 'invald-token-id')
+ .expect(401)
+ .end(done);
+ });
+
it('should skip when req.token is already present', function(done) {
var tokenStub = { id: 'stub id' };
app.use(function(req, res, next) {
req.accessToken = tokenStub;
+
next();
});
app.use(loopback.token({ model: Token }));
@@ -195,10 +237,139 @@ describe('loopback.token(options)', function() {
.expect(200)
.end(function(err, res) {
if (err) return done(err);
+
expect(res.body).to.eql(tokenStub);
+
done();
});
});
+
+ describe('loading multiple instances of token middleware', function() {
+ it('should skip when req.token is already present and no further options are set',
+ function(done) {
+ var tokenStub = { id: 'stub id' };
+ app.use(function(req, res, next) {
+ req.accessToken = tokenStub;
+
+ next();
+ });
+ app.use(loopback.token({ model: Token }));
+ app.get('/', function(req, res, next) {
+ res.send(req.accessToken);
+ });
+
+ request(app).get('/')
+ .set('Authorization', this.token.id)
+ .expect(200)
+ .end(function(err, res) {
+ if (err) return done(err);
+
+ expect(res.body).to.eql(tokenStub);
+
+ done();
+ });
+ });
+
+ it('should not overwrite valid existing token (has "id" property) ' +
+ ' when overwriteExistingToken is falsy',
+ function(done) {
+ var tokenStub = { id: 'stub id' };
+ app.use(function(req, res, next) {
+ req.accessToken = tokenStub;
+
+ next();
+ });
+ app.use(loopback.token({
+ model: Token,
+ enableDoublecheck: true,
+ }));
+ app.get('/', function(req, res, next) {
+ res.send(req.accessToken);
+ });
+
+ request(app).get('/')
+ .set('Authorization', this.token.id)
+ .expect(200)
+ .end(function(err, res) {
+ if (err) return done(err);
+
+ expect(res.body).to.eql(tokenStub);
+
+ done();
+ });
+ });
+
+ it('should overwrite invalid existing token (is !== undefined and has no "id" property) ' +
+ ' when enableDoubkecheck is true',
+ function(done) {
+ var token = this.token;
+
+ app.use(function(req, res, next) {
+ req.accessToken = null;
+ next();
+ });
+
+ app.use(loopback.token({
+ model: Token,
+ enableDoublecheck: true,
+ }));
+
+ app.get('/', function(req, res, next) {
+ res.send(req.accessToken);
+ });
+
+ request(app).get('/')
+ .set('Authorization', token.id)
+ .expect(200)
+ .end(function(err, res) {
+ if (err) return done(err);
+ expect(res.body).to.eql({
+ id: token.id,
+ ttl: token.ttl,
+ userId: token.userId,
+ created: token.created.toJSON(),
+ });
+ done();
+ });
+ });
+
+ it('should overwrite existing token when enableDoublecheck ' +
+ 'and overwriteExistingToken options are truthy',
+ function(done) {
+ var token = this.token;
+ var tokenStub = { id: 'stub id' };
+
+ app.use(function(req, res, next) {
+ req.accessToken = tokenStub;
+
+ next();
+ });
+ app.use(loopback.token({
+ model: Token,
+ enableDoublecheck: true,
+ overwriteExistingToken: true,
+ }));
+ app.get('/', function(req, res, next) {
+ res.send(req.accessToken);
+ });
+
+ request(app).get('/')
+ .set('Authorization', token.id)
+ .expect(200)
+ .end(function(err, res) {
+ if (err) return done(err);
+
+ expect(res.body).to.eql({
+ id: token.id,
+ ttl: token.ttl,
+ userId: token.userId,
+ created: token.created.toJSON(),
+ });
+
+ done();
+ });
+ });
+ });
});
describe('AccessToken', function() {
@@ -214,10 +385,38 @@ describe('AccessToken', function() {
assert(Object.prototype.toString.call(this.token.created), '[object Date]');
});
- it('should be validateable', function(done) {
- this.token.validate(function(err, isValid) {
- assert(isValid);
- done();
+ describe('.validate()', function() {
+ it('accepts valid tokens', function(done) {
+ this.token.validate(function(err, isValid) {
+ assert(isValid);
+ done();
+ });
+ });
+
+ it('rejects eternal TTL by default', function(done) {
+ this.token.ttl = -1;
+ this.token.validate(function(err, isValid) {
+ if (err) return done(err);
+ expect(isValid, 'isValid').to.equal(false);
+ done();
+ });
+ });
+
+ it('allows eternal tokens when enabled by User.allowEternalTokens',
+ function(done) {
+ var Token = givenLocalTokenModel();
+
+ // Overwrite User settings - enable eternal tokens
+ Token.app.models.User.settings.allowEternalTokens = true;
+
+ Token.create({ userId: '123', ttl: -1 }, function(err, token) {
+ if (err) return done(err);
+ token.validate(function(err, isValid) {
+ if (err) return done(err);
+ expect(isValid, 'isValid').to.equal(true);
+ done();
+ });
+ });
});
});
@@ -232,7 +431,9 @@ describe('AccessToken', function() {
Token.findForRequest(req, function(err, token) {
if (err) return done(err);
+
expect(token.id).to.eql(expectedTokenId);
+
done();
});
});
@@ -255,6 +456,9 @@ describe('AccessToken', function() {
});
describe('app.enableAuth()', function() {
+ beforeEach(function setupAuthWithModels() {
+ app.enableAuth({ dataSource: ds });
+ });
beforeEach(createTestingToken);
it('prevents remote call with 401 status on denied ACL', function(done) {
@@ -266,15 +470,17 @@ describe('app.enableAuth()', function() {
if (err) {
return done(err);
}
+
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED');
+
done();
});
});
it('prevent remote call with app setting status on denied ACL', function(done) {
- createTestAppAndRequest(this.token, {app:{aclErrorStatus:403}}, done)
+ createTestAppAndRequest(this.token, {app: {aclErrorStatus: 403}}, done)
.del('/tests/123')
.expect(403)
.set('authorization', this.token.id)
@@ -282,15 +488,17 @@ describe('app.enableAuth()', function() {
if (err) {
return done(err);
}
+
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'ACCESS_DENIED');
+
done();
});
});
it('prevent remote call with app setting status on denied ACL', function(done) {
- createTestAppAndRequest(this.token, {model:{aclErrorStatus:404}}, done)
+ createTestAppAndRequest(this.token, {model: {aclErrorStatus: 404}}, done)
.del('/tests/123')
.expect(404)
.set('authorization', this.token.id)
@@ -298,9 +506,11 @@ describe('app.enableAuth()', function() {
if (err) {
return done(err);
}
+
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'MODEL_NOT_FOUND');
+
done();
});
});
@@ -314,9 +524,11 @@ describe('app.enableAuth()', function() {
if (err) {
return done(err);
}
+
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED');
+
done();
});
});
@@ -324,7 +536,8 @@ describe('app.enableAuth()', function() {
it('stores token in the context', function(done) {
var TestModel = loopback.createModel('TestModel', { base: 'Model' });
TestModel.getToken = function(cb) {
- cb(null, loopback.getCurrentContext().get('accessToken') || null);
+ var ctx = loopback.getCurrentContext();
+ cb(null, ctx && ctx.get('accessToken') || null);
};
TestModel.remoteMethod('getToken', {
returns: { arg: 'token', type: 'object' },
@@ -347,17 +560,44 @@ describe('app.enableAuth()', function() {
.expect('Content-Type', /json/)
.end(function(err, res) {
if (err) return done(err);
+
expect(res.body.token.id).to.eql(token.id);
+
done();
});
});
+
+ // See https://github.com/strongloop/loopback-context/issues/6
+ it('checks whether context is active', function(done) {
+ var app = loopback();
+
+ app.enableAuth();
+ app.use(loopback.context());
+ app.use(session({
+ secret: 'kitty',
+ saveUninitialized: true,
+ resave: true
+ }));
+ app.use(loopback.token({ model: Token }));
+ app.get('/', function(req, res) { res.send('OK'); });
+ app.use(loopback.rest());
+
+ request(app)
+ .get('/')
+ .set('authorization', this.token.id)
+ .set('cookie', 'connect.sid=s%3AFTyno9_MbGTJuOwdh9bxsYCVxlhlulTZ.PZvp85jzLXZBCBkhCsSfuUjhij%2Fb0B1K2RYZdxSQU0c')
+ .expect(200, 'OK')
+ .end(done);
+ });
});
function createTestingToken(done) {
var test = this;
Token.create({userId: '123'}, function(err, token) {
if (err) return done(err);
+
test.token = token;
+
done();
});
}
@@ -380,8 +620,9 @@ function createTestApp(testToken, settings, done) {
}, settings.token);
var app = loopback();
+ app.set('logoutSessionsOnSensitiveChanges', true);
- app.use(loopback.cookieParser('secret'));
+ app.use(cookieParser('secret'));
app.use(loopback.token(tokenSettings));
app.get('/token', function(req, res) {
res.cookie('authorization', testToken.id, {signed: true});
@@ -439,3 +680,17 @@ function createTestApp(testToken, settings, done) {
return app;
}
+
+function givenLocalTokenModel() {
+ var app = loopback({ localRegistry: true, loadBuiltinModels: true });
+ app.set('logoutSessionsOnSensitiveChanges', true);
+ app.dataSource('db', { connector: 'memory' });
+
+ var User = app.registry.getModel('User');
+ app.model(User, { dataSource: 'db' });
+
+ var Token = app.registry.getModel('AccessToken');
+ app.model(Token, { dataSource: 'db' });
+
+ return Token;
+}
diff --git a/test/acl.test.js b/test/acl.test.js
index d8706eec3..430d71fe2 100644
--- a/test/acl.test.js
+++ b/test/acl.test.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var assert = require('assert');
var loopback = require('../index');
var Scope = loopback.Scope;
@@ -358,19 +363,17 @@ describe('security ACLs', function() {
});
describe('access check', function() {
- var app;
- before(function() {
- app = loopback();
- app.use(loopback.rest());
- app.enableAuth();
- app.dataSource('test', {connector: 'memory'});
- });
-
it('should occur before other remote hooks', function(done) {
- var MyTestModel = app.model('MyTestModel', {base: 'PersistedModel', dataSource: 'test'});
+ var app = loopback();
+ var MyTestModel = app.registry.createModel('MyTestModel');
var checkAccessCalled = false;
var beforeHookCalled = false;
+ app.use(loopback.rest());
+ app.enableAuth();
+ app.dataSource('test', { connector: 'memory' });
+ app.model(MyTestModel, { dataSource: 'test' });
+
// fake / spy on the checkAccess method
MyTestModel.checkAccess = function() {
var cb = arguments[arguments.length - 1];
@@ -382,7 +385,9 @@ describe('access check', function() {
MyTestModel.beforeRemote('find', function(ctx, next) {
// ensure this is called after checkAccess
if (!checkAccessCalled) return done(new Error('incorrect order'));
+
beforeHookCalled = true;
+
next();
});
@@ -391,6 +396,7 @@ describe('access check', function() {
.end(function(err, result) {
assert(beforeHookCalled, 'the before hook should be called');
assert(checkAccessCalled, 'checkAccess should have been called');
+
done();
});
});
diff --git a/test/app.test.js b/test/app.test.js
index 2701ef9d2..7df1e7937 100644
--- a/test/app.test.js
+++ b/test/app.test.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
/*jshint -W030 */
var async = require('async');
@@ -9,6 +14,7 @@ var loopback = require('../');
var PersistedModel = loopback.PersistedModel;
var describe = require('./util/describe');
+var expect = require('chai').expect;
var it = require('./util/it');
describe('app', function() {
@@ -34,10 +40,12 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql([
'initial', 'session', 'auth', 'parse',
'main', 'routes', 'files', 'final'
]);
+
done();
});
});
@@ -48,7 +56,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['first', 'second']);
+
done();
});
});
@@ -60,7 +70,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['routes:before', 'main', 'routes:after']);
+
done();
});
});
@@ -80,7 +92,9 @@ describe('app', function() {
expect(found).have.property('phase', 'routes:before');
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['my-handler', 'extra-handler']);
+
done();
});
});
@@ -98,7 +112,9 @@ describe('app', function() {
expect(found).have.property('phase', 'routes:before');
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['my-handler']);
+
done();
});
});
@@ -116,7 +132,9 @@ describe('app', function() {
expect(found).have.property('phase', 'routes:before');
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['my-handler']);
+
done();
});
});
@@ -126,6 +144,7 @@ describe('app', function() {
app.middleware('initial', function(req, res, next) {
steps.push('initial');
+
next(expectedError);
});
@@ -133,12 +152,15 @@ describe('app', function() {
app.use(function errorHandler(err, req, res, next) {
expect(err).to.equal(expectedError);
steps.push('error');
+
next();
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['initial', 'error']);
+
done();
});
});
@@ -152,6 +174,7 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) {
expect(err).to.equal(expectedError);
+
done();
});
});
@@ -170,12 +193,15 @@ describe('app', function() {
app.middleware('initial', function(err, req, res, next) {
handledError = err;
+
next();
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(handledError).to.equal(expectedError);
+
done();
});
});
@@ -188,7 +214,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['/scope', '/scope/item']);
+
done();
});
});
@@ -201,7 +229,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['/a', '/b']);
+
done();
});
});
@@ -214,7 +244,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['/a', '/b', '/scope']);
+
done();
});
});
@@ -222,12 +254,15 @@ describe('app', function() {
it('sets req.url to a sub-path', function(done) {
app.middleware('initial', ['/scope'], function(req, res, next) {
steps.push(req.url);
+
next();
});
executeMiddlewareHandlers(app, '/scope/id', function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['/id']);
+
done();
});
});
@@ -239,11 +274,13 @@ describe('app', function() {
app.middleware('initial', function(rq, rs, next) {
req = rq;
res = rs;
+
next();
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([
'accepts',
'get',
@@ -273,12 +310,15 @@ describe('app', function() {
var reqProps;
app.middleware('initial', function(req, res, next) {
reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl };
+
next();
});
executeMiddlewareHandlers(app, '/test/url', function(err) {
if (err) return done(err);
+
expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' });
+
done();
});
});
@@ -290,7 +330,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, '/test', function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['route', 'files']);
+
done();
});
});
@@ -310,7 +352,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) {
if (err) return done;
+
expect(steps).to.eql(numbers);
+
done();
});
});
@@ -324,6 +368,7 @@ describe('app', function() {
mountpath: req.app.mountpath,
parent: req.app.parent
};
+
next();
});
subapp.on('mount', function() { mountWasEmitted = true; });
@@ -332,11 +377,13 @@ describe('app', function() {
executeMiddlewareHandlers(app, '/mountpath/test', function(err) {
if (err) return done(err);
+
expect(mountWasEmitted, 'mountWasEmitted').to.be.true;
expect(data).to.eql({
mountpath: '/mountpath',
parent: app
});
+
done();
});
});
@@ -350,25 +397,30 @@ describe('app', function() {
subapp.use(function verifyTestAssumptions(req, res, next) {
expect(req.__proto__).to.not.equal(expected.req);
expect(res.__proto__).to.not.equal(expected.res);
+
next();
});
app.middleware('initial', function saveOriginalValues(req, res, next) {
expected.req = req.__proto__;
expected.res = res.__proto__;
+
next();
});
app.middleware('routes', subapp);
app.middleware('final', function saveActualValues(req, res, next) {
actual.req = req.__proto__;
actual.res = res.__proto__;
+
next();
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(actual.req, 'req').to.equal(expected.req);
expect(actual.res, 'res').to.equal(expected.res);
+
done();
});
});
@@ -383,6 +435,7 @@ describe('app', function() {
function pathSavingHandler() {
return function(req, res, next) {
steps.push(req.originalUrl);
+
next();
};
}
@@ -406,6 +459,7 @@ describe('app', function() {
var args = Array.prototype.slice.apply(arguments);
return function(req, res, next) {
steps.push(args);
+
next();
};
};
@@ -456,12 +510,14 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql([
['before'],
[expectedConfig],
['after', 2],
[{x: 1}]
]);
+
done();
});
});
@@ -472,6 +528,7 @@ describe('app', function() {
function factory() {
return function(req, res, next) {
steps.push(req.originalUrl);
+
next();
};
},
@@ -485,7 +542,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) {
if (err) return done(err);
+
expect(steps).to.eql(['/a', '/b', '/scope']);
+
done();
});
});
@@ -542,24 +601,27 @@ describe('app', function() {
names.forEach(function(it) {
app.middleware(it, function(req, res, next) {
steps.push(it);
+
next();
});
});
executeMiddlewareHandlers(app, function(err) {
if (err) return done(err);
+
expect(steps).to.eql(names);
+
done();
});
}
});
describe('app.model(Model)', function() {
- var app;
- var db;
+ var app, db, MyTestModel;
beforeEach(function() {
app = loopback();
- db = loopback.createDataSource({connector: loopback.Memory});
+ db = loopback.createDataSource({ connector: loopback.Memory });
+ MyTestModel = app.registry.createModel('MyTestModel', {}, {base: 'Model'});
});
it('Expose a `Model` to remote clients', function() {
@@ -570,8 +632,8 @@ describe('app', function() {
expect(app.models()).to.eql([Color]);
});
- it('uses singlar name as app.remoteObjects() key', function() {
- var Color = PersistedModel.extend('color', {name: String});
+ it('uses singular name as app.remoteObjects() key', function() {
+ var Color = PersistedModel.extend('color', { name: String });
app.model(Color);
Color.attachTo(db);
expect(app.remoteObjects()).to.eql({ color: Color });
@@ -606,6 +668,22 @@ describe('app', function() {
expect(remotedClass).to.eql(Color.sharedClass);
});
+ it('emits a `remoteMethodDisabled` event', function() {
+ var Color = PersistedModel.extend('color', { name: String });
+ Color.shared = true;
+ var remoteMethodDisabledClass, disabledRemoteMethod;
+ app.on('remoteMethodDisabled', function(sharedClass, methodName) {
+ remoteMethodDisabledClass = sharedClass;
+ disabledRemoteMethod = methodName;
+ });
+ app.model(Color);
+ app.models.Color.disableRemoteMethodByName('findOne');
+ expect(remoteMethodDisabledClass).to.exist;
+ expect(remoteMethodDisabledClass).to.eql(Color.sharedClass);
+ expect(disabledRemoteMethod).to.exist;
+ expect(disabledRemoteMethod).to.eql('findOne');
+ });
+
it.onServer('updates REST API when a new model is added', function(done) {
app.use(loopback.rest());
request(app).get('/colors').expect(404, function(err, res) {
@@ -617,18 +695,22 @@ describe('app', function() {
});
});
- it('accepts null dataSource', function() {
- app.model('MyTestModel', { dataSource: null });
+ it('accepts null dataSource', function(done) {
+ app.model(MyTestModel, { dataSource: null });
+ expect(MyTestModel.dataSource).to.eql(null);
+ done();
});
- it('accepts false dataSource', function() {
- app.model('MyTestModel', { dataSource: false });
+ it('accepts false dataSource', function(done) {
+ app.model(MyTestModel, { dataSource: false });
+ expect(MyTestModel.getDataSource()).to.eql(null);
+ done();
});
- it('should not require dataSource', function() {
- app.model('MyTestModel', {});
+ it('does not require dataSource', function(done) {
+ app.model(MyTestModel);
+ done();
});
-
});
describe('app.model(name, config)', function() {
@@ -636,6 +718,7 @@ describe('app', function() {
beforeEach(function() {
app = loopback();
+ app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', {
connector: 'memory'
});
@@ -689,7 +772,6 @@ describe('app', function() {
expect(app.models.foo.app).to.equal(app);
expect(app.models.foo.shared).to.equal(true);
});
-
});
describe('app.model(ModelCtor, config)', function() {
@@ -702,7 +784,8 @@ describe('app', function() {
}
assert(!previousModel || !previousModel.dataSource);
- app.model('TestModel', { dataSource: 'db' });
+ var TestModel = app.registry.createModel('TestModel');
+ app.model(TestModel, { dataSource: 'db' });
expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db);
});
});
@@ -710,7 +793,8 @@ describe('app', function() {
describe('app.models', function() {
it('is unique per app instance', function() {
app.dataSource('db', { connector: 'memory' });
- var Color = app.model('Color', { dataSource: 'db' });
+ var Color = app.registry.createModel('Color');
+ app.model(Color, { dataSource: 'db' });
expect(app.models.Color).to.equal(Color);
var anotherApp = loopback();
expect(anotherApp.models.Color).to.equal(undefined);
@@ -732,6 +816,22 @@ describe('app', function() {
app.dataSource('custom', { connector: 'custom' });
expect(app.dataSources.custom.name).to.equal(loopback.Memory.name);
});
+
+ it('adds data source name to error messages', function() {
+ app.connector('throwing', {
+ initialize: function() { throw new Error('expected test error'); },
+ });
+
+ expect(function() {
+ app.dataSource('bad-ds', { connector: 'throwing' });
+ }).to.throw(/bad-ds.*throwing/);
+ });
+
+ it('adds app reference to the data source object', function() {
+ app.dataSource('ds', { connector: 'memory' });
+ expect(app.datasources.ds.app).to.not.equal(undefined);
+ expect(app.datasources.ds.app).to.equal(app);
+ });
});
describe.onServer('listen()', function() {
@@ -755,6 +855,7 @@ describe('app', function() {
app.listen(function() {
expect(app.get('port'), 'port').to.not.equal(0);
+
done();
});
});
@@ -768,6 +869,7 @@ describe('app', function() {
var host = process.platform === 'win32' ? 'localhost' : app.get('host');
var expectedUrl = 'http://' + host + ':' + app.get('port') + '/';
expect(app.get('url'), 'url').to.equal(expectedUrl);
+
done();
});
});
@@ -778,6 +880,7 @@ describe('app', function() {
app.listen(0, '127.0.0.1', function() {
expect(app.get('port'), 'port').to.not.equal(0).and.not.equal(1);
expect(this.address().address).to.equal('127.0.0.1');
+
done();
});
});
@@ -788,6 +891,7 @@ describe('app', function() {
app.set('port', 1);
app.listen(0).on('listening', function() {
expect(app.get('port'), 'port') .to.not.equal(0).and.not.equal(1);
+
done();
});
}
@@ -802,6 +906,7 @@ describe('app', function() {
app.listen()
.on('listening', function() {
expect(this.address().address).to.equal('127.0.0.1');
+
done();
});
});
@@ -818,6 +923,7 @@ describe('app', function() {
var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping'];
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
require('../lib/builtin-models')(app.registry);
+ app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' });
app.enableAuth({ dataSource: 'db' });
@@ -833,6 +939,7 @@ describe('app', function() {
it('detects already configured subclass of a required model', function() {
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
+ app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' });
var Customer = app.registry.createModel('Customer', {}, { base: 'User' });
app.model(Customer, { dataSource: 'db' });
@@ -853,18 +960,14 @@ describe('app', function() {
.end(function(err, res) {
if (err) return done(err);
- assert.equal(typeof res.body, 'object');
- assert(res.body.started);
- // The number can be 0
- assert(res.body.uptime !== undefined);
+ expect(res.body).to.be.an('object');
+ expect(res.body).to.have.property('started');
+ expect(res.body.uptime, 'uptime').to.be.gte(0);
var elapsed = Date.now() - Number(new Date(res.body.started));
- // elapsed should be a positive number...
- assert(elapsed >= 0);
-
- // less than 100 milliseconds
- assert(elapsed < 100);
+ // elapsed should be a small positive number...
+ expect(elapsed, 'elapsed').to.be.within(0, 300);
done();
});
@@ -957,8 +1060,18 @@ describe('app', function() {
});
function executeMiddlewareHandlers(app, urlPath, callback) {
+ var handlerError;
var server = http.createServer(function(req, res) {
- app.handle(req, res, callback);
+ app.handle(req, res, function(err) {
+ if (err) {
+ handlerError = err;
+ res.statusCode = err.status || err.statusCode || 500;
+ res.end(err.stack || err);
+ } else {
+ res.statusCode = 204;
+ res.end();
+ }
+ });
});
if (callback === undefined && typeof urlPath === 'function') {
@@ -969,6 +1082,6 @@ function executeMiddlewareHandlers(app, urlPath, callback) {
request(server)
.get(urlPath)
.end(function(err) {
- if (err) return callback(err);
+ callback(handlerError || err);
});
}
diff --git a/test/change-stream.test.js b/test/change-stream.test.js
index ab7405214..86dfc77fd 100644
--- a/test/change-stream.test.js
+++ b/test/change-stream.test.js
@@ -1,10 +1,16 @@
+// Copyright IBM Corp. 2015,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
describe('PersistedModel.createChangeStream()', function() {
describe('configured to source changes locally', function() {
before(function() {
var test = this;
- var app = loopback({localRegistry: true});
- var ds = app.dataSource('ds', {connector: 'memory'});
- this.Score = app.model('Score', {
+ var app = loopback({ localRegistry: true });
+ var ds = app.dataSource('ds', { connector: 'memory' });
+ var Score = app.registry.createModel('Score');
+ this.Score = app.model(Score, {
dataSource: 'ds',
changeDataSource: false // use only local observers
});
@@ -17,6 +23,7 @@ describe('PersistedModel.createChangeStream()', function() {
changes.on('data', function(change) {
expect(change.type).to.equal('create');
changes.destroy();
+
done();
});
@@ -31,6 +38,7 @@ describe('PersistedModel.createChangeStream()', function() {
changes.on('data', function(change) {
expect(change.type).to.equal('update');
changes.destroy();
+
done();
});
newScore.updateAttributes({
@@ -47,6 +55,7 @@ describe('PersistedModel.createChangeStream()', function() {
changes.on('data', function(change) {
expect(change.type).to.equal('remove');
changes.destroy();
+
done();
});
diff --git a/test/change.test.js b/test/change.test.js
index 55d551c72..220d8055f 100644
--- a/test/change.test.js
+++ b/test/change.test.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var async = require('async');
var expect = require('chai').expect;
@@ -28,9 +33,11 @@ describe('Change', function() {
};
TestModel.create(test.data, function(err, model) {
if (err) return done(err);
+
test.model = model;
test.modelId = model.id;
test.revisionForModel = Change.revisionForInst(model);
+
done();
});
});
@@ -61,6 +68,7 @@ describe('Change', function() {
var test = this;
Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) {
if (err) return done(err);
+
done();
});
});
@@ -69,6 +77,7 @@ describe('Change', function() {
var test = this;
Change.find(function(err, trackedChanges) {
assert.equal(trackedChanges[0].modelId, test.modelId.toString());
+
done();
});
});
@@ -76,12 +85,47 @@ describe('Change', function() {
it('should only create one change', function(done) {
Change.count(function(err, count) {
assert.equal(count, 1);
+
done();
});
});
});
});
+ describe('Change.rectifyModelChanges - promise variant', function() {
+ describe('using an existing untracked model', function() {
+ beforeEach(function(done) {
+ var test = this;
+ Change.rectifyModelChanges(this.modelName, [this.modelId])
+ .then(function(trackedChanges) {
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should create an entry', function(done) {
+ var test = this;
+ Change.find()
+ .then(function(trackedChanges) {
+ assert.equal(trackedChanges[0].modelId, test.modelId.toString());
+
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should only create one change', function(done) {
+ Change.count()
+ .then(function(count) {
+ assert.equal(count, 1);
+
+ done();
+ })
+ .catch(done);
+ });
+ });
+ });
+
describe('Change.findOrCreateChange(modelName, modelId, callback)', function() {
describe('when a change doesnt exist', function() {
@@ -89,7 +133,9 @@ describe('Change', function() {
var test = this;
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
if (err) return done(err);
+
test.result = result;
+
done();
});
});
@@ -98,7 +144,33 @@ describe('Change', function() {
var test = this;
Change.findById(this.result.id, function(err, change) {
if (err) return done(err);
+
assert.equal(change.id, test.result.id);
+
+ done();
+ });
+ });
+ });
+
+ describe('when a change doesnt exist - promise variant', function() {
+ beforeEach(function(done) {
+ var test = this;
+ Change.findOrCreateChange(this.modelName, this.modelId)
+ .then(function(result) {
+ test.result = result;
+
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should create an entry', function(done) {
+ var test = this;
+ Change.findById(this.result.id, function(err, change) {
+ if (err) return done(err);
+
+ assert.equal(change.id, test.result.id);
+
done();
});
});
@@ -112,6 +184,7 @@ describe('Change', function() {
modelId: test.modelId
}, function(err, change) {
test.existingChange = change;
+
done();
});
});
@@ -120,7 +193,9 @@ describe('Change', function() {
var test = this;
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
if (err) return done(err);
+
test.result = result;
+
done();
});
});
@@ -128,6 +203,7 @@ describe('Change', function() {
it('should find the entry', function(done) {
var test = this;
assert.equal(test.existingChange.id, test.result.id);
+
done();
});
});
@@ -143,6 +219,7 @@ describe('Change', function() {
},
function(err, ch) {
change = ch;
+
done(err);
});
});
@@ -151,6 +228,7 @@ describe('Change', function() {
var test = this;
change.rectify(function(err, ch) {
assert.equal(ch.rev, test.revisionForModel);
+
done();
});
});
@@ -174,6 +252,7 @@ describe('Change', function() {
expect(change.type(), 'type').to.equal('update');
expect(change.prev, 'prev').to.equal(originalRev);
expect(change.rev, 'rev').to.equal(test.revisionForModel);
+
next();
}
], done);
@@ -185,7 +264,9 @@ describe('Change', function() {
function checkpoint(next) {
TestModel.checkpoint(function(err, inst) {
if (err) return next(err);
+
cp = inst.seq;
+
next();
});
}
@@ -196,6 +277,7 @@ describe('Change', function() {
model.name += 'updated';
model.save(function(err) {
test.revisionForModel = Change.revisionForInst(model);
+
next(err);
});
}
@@ -211,14 +293,40 @@ describe('Change', function() {
change.rectify(function(err, c) {
if (err) return done(err);
+
expect(c.rev, 'rev').to.equal(originalRev); // sanity check
expect(c.checkpoint, 'checkpoint').to.equal(originalCheckpoint);
+
done();
});
});
});
});
+ describe('change.rectify - promise variant', function() {
+ var change;
+ beforeEach(function(done) {
+ Change.findOrCreateChange(this.modelName, this.modelId)
+ .then(function(ch) {
+ change = ch;
+
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should create a new change with the correct revision', function(done) {
+ var test = this;
+ change.rectify()
+ .then(function(ch) {
+ assert.equal(ch.rev, test.revisionForModel);
+
+ done();
+ })
+ .catch(done);
+ });
+ });
+
describe('change.currentRevision(callback)', function() {
it('should get the correct revision', function(done) {
var test = this;
@@ -229,11 +337,30 @@ describe('Change', function() {
change.currentRevision(function(err, rev) {
assert.equal(rev, test.revisionForModel);
+
done();
});
});
});
+ describe('change.currentRevision - promise variant', function() {
+ it('should get the correct revision', function(done) {
+ var test = this;
+ var change = new Change({
+ modelName: this.modelName,
+ modelId: this.modelId
+ });
+
+ change.currentRevision()
+ .then(function(rev) {
+ assert.equal(rev, test.revisionForModel);
+
+ done();
+ })
+ .catch(done);
+ });
+ });
+
describe('Change.hash(str)', function() {
// todo(ritch) test other hashing algorithms
it('should hash the given string', function() {
@@ -368,12 +495,34 @@ describe('Change', function() {
Change.diff(this.modelName, 0, remoteChanges, function(err, diff) {
if (err) return done(err);
+
assert.equal(diff.deltas.length, 1);
assert.equal(diff.conflicts.length, 1);
+
done();
});
});
+ it('should return delta and conflict lists - promise variant', function(done) {
+ var remoteChanges = [
+ // an update => should result in a delta
+ {rev: 'foo2', prev: 'foo', modelName: this.modelName, modelId: 9, checkpoint: 1},
+ // no change => should not result in a delta / conflict
+ {rev: 'bar', prev: 'bar', modelName: this.modelName, modelId: 10, checkpoint: 1},
+ // a conflict => should result in a conflict
+ {rev: 'bat2', prev: 'bat0', modelName: this.modelName, modelId: 11, checkpoint: 1},
+ ];
+
+ Change.diff(this.modelName, 0, remoteChanges)
+ .then(function(diff) {
+ assert.equal(diff.deltas.length, 1);
+ assert.equal(diff.conflicts.length, 1);
+
+ done();
+ })
+ .catch(done);
+ });
+
it('should set "prev" to local revision in non-conflicting delta', function(done) {
var updateRecord = {
rev: 'foo-new',
@@ -384,6 +533,7 @@ describe('Change', function() {
};
Change.diff(this.modelName, 0, [updateRecord], function(err, diff) {
if (err) return done(err);
+
expect(diff.conflicts, 'conflicts').to.have.length(0);
expect(diff.deltas, 'deltas').to.have.length(1);
var actual = diff.deltas[0].toObject();
@@ -395,6 +545,7 @@ describe('Change', function() {
prev: 'foo', // this is the current local revision
rev: 'foo-new',
});
+
done();
});
});
@@ -411,6 +562,7 @@ describe('Change', function() {
// with rev=foo CP=1
Change.diff(this.modelName, 2, [updateRecord], function(err, diff) {
if (err) return done(err);
+
expect(diff.conflicts, 'conflicts').to.have.length(0);
expect(diff.deltas, 'deltas').to.have.length(1);
var actual = diff.deltas[0].toObject();
@@ -422,6 +574,7 @@ describe('Change', function() {
prev: 'foo', // this is the current local revision
rev: 'foo-new',
});
+
done();
});
});
@@ -437,6 +590,7 @@ describe('Change', function() {
Change.diff(this.modelName, 0, [updateRecord], function(err, diff) {
if (err) return done(err);
+
expect(diff.conflicts).to.have.length(0);
expect(diff.deltas).to.have.length(1);
var actual = diff.deltas[0].toObject();
@@ -448,6 +602,7 @@ describe('Change', function() {
prev: null, // this is the current local revision
rev: 'new-rev',
});
+
done();
});
});
diff --git a/test/checkpoint.test.js b/test/checkpoint.test.js
index 98e248efb..b5b9093ae 100644
--- a/test/checkpoint.test.js
+++ b/test/checkpoint.test.js
@@ -1,28 +1,98 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var async = require('async');
var loopback = require('../');
+var expect = require('chai').expect;
-// create a unique Checkpoint model
var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint');
-var memory = loopback.createDataSource({
- connector: loopback.Memory
-});
-Checkpoint.attachTo(memory);
-
describe('Checkpoint', function() {
- describe('current()', function() {
+ describe('bumpLastSeq() and current()', function() {
+ beforeEach(function() {
+ var memory = loopback.createDataSource({
+ connector: loopback.Memory
+ });
+ Checkpoint.attachTo(memory);
+ });
+
it('returns the highest `seq` value', function(done) {
async.series([
- Checkpoint.create.bind(Checkpoint),
- Checkpoint.create.bind(Checkpoint),
+ Checkpoint.bumpLastSeq.bind(Checkpoint),
+ Checkpoint.bumpLastSeq.bind(Checkpoint),
function(next) {
Checkpoint.current(function(err, seq) {
if (err) next(err);
+
expect(seq).to.equal(3);
+
next();
});
}
], done);
});
+
+ it('Should be no race condition for current() when calling in parallel', function(done) {
+ async.parallel([
+ function(next) { Checkpoint.current(next); },
+ function(next) { Checkpoint.current(next); }
+ ], function(err, list) {
+ if (err) return done(err);
+
+ Checkpoint.find(function(err, data) {
+ if (err) return done(err);
+
+ expect(data).to.have.length(1);
+
+ done();
+ });
+ });
+ });
+
+ it('Should be no race condition for bumpLastSeq() when calling in parallel', function(done) {
+ async.parallel([
+ function(next) { Checkpoint.bumpLastSeq(next); },
+ function(next) { Checkpoint.bumpLastSeq(next); }
+ ], function(err, list) {
+ if (err) return done(err);
+
+ Checkpoint.find(function(err, data) {
+ if (err) return done(err);
+ // The invariant "we have at most 1 checkpoint instance" is preserved
+ // even when multiple calls are made in parallel
+ expect(data).to.have.length(1);
+ // There is a race condition here, we could end up with both 2 or 3 as the "seq".
+ // The current implementation of the memory connector always yields 2 though.
+ expect(data[0].seq).to.equal(2);
+ // In this particular case, since the new last seq is always 2, both results
+ // should be 2.
+ expect(list.map(function(it) {return it.seq;}))
+ .to.eql([2, 2]);
+
+ done();
+ });
+ });
+ });
+
+ it('Checkpoint.current() for non existing checkpoint should initialize checkpoint', function(done) {
+ Checkpoint.current(function(err, seq) {
+ expect(seq).to.equal(1);
+
+ done(err);
+ });
+ });
+
+ it('bumpLastSeq() works when singleton instance does not exists yet', function(done) {
+ Checkpoint.bumpLastSeq(function(err, cp) {
+ // We expect `seq` to be 2 since `checkpoint` does not exist and
+ // `bumpLastSeq` for the first time not only initializes it to one,
+ // but also increments the initialized value by one.
+ expect(cp.seq).to.equal(2);
+
+ done(err);
+ });
+ });
});
});
diff --git a/test/context-options.test.js b/test/context-options.test.js
new file mode 100644
index 000000000..ac8f50412
--- /dev/null
+++ b/test/context-options.test.js
@@ -0,0 +1,454 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+'use strict';
+
+var expect = require('chai').expect;
+var loopback = require('..');
+var supertest = require('supertest-as-promised')(require('bluebird'));
+
+describe('OptionsFromRemotingContext', function() {
+ var app, request, accessToken, userId, Product, actualOptions;
+
+ beforeEach(setupAppAndRequest);
+ beforeEach(resetActualOptions);
+
+ context('when making updates via REST', function() {
+ beforeEach(observeOptionsBeforeSave);
+
+ it('injects options to create()', function() {
+ return request.post('/products')
+ .send({name: 'Pen'})
+ .expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to patchOrCreate()', function() {
+ return request.patch('/products')
+ .send({id: 1, name: 'Pen'})
+ .expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to replaceOrCreate()', function() {
+ return request.put('/products')
+ .send({id: 1, name: 'Pen'})
+ .expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to patchOrCreateWithWhere()', function() {
+ return request.post('/products/upsertWithWhere?where[name]=Pen')
+ .send({name: 'Pencil'})
+ .expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to replaceById()', function() {
+ return Product.create({id: 1, name: 'Pen'})
+ .then(function(p) {
+ return request.put('/products/1')
+ .send({name: 'Pencil'})
+ .expect(200);
+ })
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to prototype.patchAttributes()', function() {
+ return Product.create({id: 1, name: 'Pen'})
+ .then(function(p) {
+ return request.patch('/products/1')
+ .send({name: 'Pencil'})
+ .expect(200);
+ })
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to updateAll()', function() {
+ return request.post('/products/update?where[name]=Pen')
+ .send({name: 'Pencil'})
+ .expect(200)
+ .then(expectInjectedOptions);
+ });
+ });
+
+ context('when deleting via REST', function() {
+ beforeEach(observeOptionsBeforeDelete);
+
+ it('injects options to deleteById()', function() {
+ return Product.create({id: 1, name: 'Pen'})
+ .then(function(p) {
+ return request.delete('/products/1').expect(200);
+ })
+ .then(expectInjectedOptions);
+ });
+ });
+
+ context('when querying via REST', function() {
+ beforeEach(observeOptionsOnAccess);
+ beforeEach(givenProductId1);
+
+ it('injects options to find()', function() {
+ return request.get('/products').expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to findById()', function() {
+ return request.get('/products/1').expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to findOne()', function() {
+ return request.get('/products/findOne?where[id]=1').expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to exists()', function() {
+ return request.head('/products/1').expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to count()', function() {
+ return request.get('/products/count').expect(200)
+ .then(expectInjectedOptions);
+ });
+ });
+
+ context('when invoking prototype methods', function() {
+ beforeEach(observeOptionsOnAccess);
+ beforeEach(givenProductId1);
+
+ it('injects options to sharedCtor', function() {
+ Product.prototype.dummy = function(cb) { cb(); };
+ Product.remoteMethod('dummy', {isStatic: false});
+ return request.post('/products/1/dummy').expect(204)
+ .then(expectInjectedOptions);
+ });
+ });
+
+ it('honours injectOptionsFromRemoteContext in sharedCtor', function() {
+ var settings = {
+ forceId: false,
+ injectOptionsFromRemoteContext: false,
+ };
+ var TestModel = app.registry.createModel('TestModel', {}, settings);
+ app.model(TestModel, {dataSource: 'db'});
+
+ TestModel.prototype.dummy = function(cb) { cb(); };
+ TestModel.remoteMethod('dummy', {isStatic: false});
+
+ observeOptionsOnAccess(TestModel);
+
+ return TestModel.create({id: 1})
+ .then(function() {
+ return request.post('/TestModels/1/dummy').expect(204);
+ })
+ .then(function() {
+ expect(actualOptions).to.eql({});
+ });
+ });
+
+ // Catch: because relations methods are defined on "modelFrom",
+ // they will invoke createOptionsFromRemotingContext on "modelFrom" too,
+ // despite the fact that under the hood a method on "modelTo" is called.
+
+ context('hasManyThrough', function() {
+ var Category, ThroughModel;
+
+ beforeEach(givenCategoryHasManyProductsThroughAnotherModel);
+ beforeEach(givenCategoryAndProduct);
+
+ it('injects options to findById', function() {
+ observeOptionsOnAccess(Product);
+ return request.get('/categories/1/products/1').expect(200)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to destroyById', function() {
+ observeOptionsBeforeDelete(Product);
+ return request.del('/categories/1/products/1').expect(204)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to updateById', function() {
+ observeOptionsBeforeSave(Product);
+ return request.put('/categories/1/products/1')
+ .send({description: 'a description'})
+ .expect(200)
+ .then(expectInjectedOptions);
+ });
+
+ context('through-model operations', function() {
+ it('injects options to link', function() {
+ observeOptionsBeforeSave(ThroughModel);
+ return Product.create({id: 2, name: 'Car2'})
+ .then(function() {
+ return request.put('/categories/1/products/rel/2')
+ .send({description: 'a description'})
+ .expect(200);
+ })
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to unlink', function() {
+ observeOptionsBeforeDelete(ThroughModel);
+ return request.del('/categories/1/products/rel/1').expect(204)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to exists', function() {
+ observeOptionsOnAccess(ThroughModel);
+ return request.head('/categories/1/products/rel/1').expect(200)
+ .then(expectOptionsInjectedFromCategory);
+ });
+ });
+
+ context('scope operations', function() {
+ it('injects options to get', function() {
+ observeOptionsOnAccess(Product);
+ return request.get('/categories/1/products').expect(200)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to create', function() {
+ observeOptionsBeforeSave(Product);
+ return request.post('/categories/1/products')
+ .send({name: 'Pen'})
+ .expect(200)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to delete', function() {
+ observeOptionsBeforeDelete(ThroughModel);
+ return request.del('/categories/1/products').expect(204)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to count', function() {
+ observeOptionsOnAccess(ThroughModel);
+ return request.get('/categories/1/products/count').expect(200)
+ .then(expectOptionsInjectedFromCategory);
+ });
+ });
+
+ function givenCategoryHasManyProductsThroughAnotherModel() {
+ var settings = {
+ forceId: false,
+ replaceOnPUT: true,
+ injectOptionsFromRemoteContext: true,
+ };
+ Category = app.registry.createModel(
+ 'Category',
+ {name: String},
+ settings);
+
+ app.model(Category, {dataSource: 'db'});
+ // This is a shortcut for creating CategoryProduct "through" model
+ Category.hasAndBelongsToMany(Product);
+
+ Category.createOptionsFromRemotingContext = function(ctx) {
+ return {injectedFrom: 'Category'};
+ };
+
+ ThroughModel = app.registry.getModel('CategoryProduct');
+ }
+
+ function givenCategoryAndProduct() {
+ return Category.create({id: 1, name: 'First Category'})
+ .then(function(cat) {
+ return cat.products.create({id: 1, name: 'Pen'});
+ });
+ }
+
+ function expectOptionsInjectedFromCategory() {
+ expect(actualOptions).to.have.property('injectedFrom', 'Category');
+ }
+ });
+
+ context('hasOne', function() {
+ var Category;
+
+ beforeEach(givenCategoryHasOneProduct);
+ beforeEach(givenCategoryId1);
+
+ it('injects options to get', function() {
+ observeOptionsOnAccess(Product);
+ return givenProductInCategory1()
+ .then(function() {
+ return request.get('/categories/1/product').expect(200);
+ })
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to create', function() {
+ observeOptionsBeforeSave(Product);
+ return request.post('/categories/1/product')
+ .send({name: 'Pen'})
+ .expect(200)
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ it('injects options to update', function() {
+ return givenProductInCategory1()
+ .then(function() {
+ observeOptionsBeforeSave(Product);
+ return request.put('/categories/1/product')
+ .send({description: 'a description'})
+ .expect(200);
+ })
+ .then(expectInjectedOptions);
+ });
+
+ it('injects options to destroy', function() {
+ observeOptionsBeforeDelete(Product);
+ return givenProductInCategory1()
+ .then(function() {
+ return request.del('/categories/1/product').expect(204);
+ })
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ function givenCategoryHasOneProduct() {
+ var settings = {
+ forceId: false,
+ replaceOnPUT: true,
+ injectOptionsFromRemoteContext: true,
+ };
+ Category = app.registry.createModel(
+ 'Category',
+ {name: String},
+ settings);
+
+ app.model(Category, {dataSource: 'db'});
+ Category.hasOne(Product);
+
+ Category.createOptionsFromRemotingContext = function(ctx) {
+ return {injectedFrom: 'Category'};
+ };
+ }
+
+ function givenCategoryId1() {
+ return Category.create({id: 1, name: 'First Category'});
+ }
+
+ function givenProductInCategory1() {
+ return Product.create({id: 1, name: 'Pen', categoryId: 1});
+ }
+
+ function expectOptionsInjectedFromCategory() {
+ expect(actualOptions).to.have.property('injectedFrom', 'Category');
+ }
+ });
+
+ context('belongsTo', function() {
+ var Category;
+
+ beforeEach(givenCategoryBelongsToProduct);
+
+ it('injects options to get', function() {
+ observeOptionsOnAccess(Product);
+ return Product.create({id: 1, name: 'Pen'})
+ .then(function() {
+ return Category.create({id: 1, name: 'a name', productId: 1});
+ })
+ .then(function() {
+ return request.get('/categories/1/product').expect(200);
+ })
+ .then(expectOptionsInjectedFromCategory);
+ });
+
+ function givenCategoryBelongsToProduct() {
+ var settings = {
+ forceId: false,
+ replaceOnPUT: true,
+ injectOptionsFromRemoteContext: true,
+ };
+ Category = app.registry.createModel(
+ 'Category',
+ {name: String},
+ settings);
+
+ app.model(Category, {dataSource: 'db'});
+ Category.belongsTo(Product);
+
+ Category.createOptionsFromRemotingContext = function(ctx) {
+ return {injectedFrom: 'Category'};
+ };
+ }
+
+ function givenCategoryId1() {
+ return Category.create({id: 1, name: 'First Category'});
+ }
+
+ function givenProductInCategory1() {
+ return Product.create({id: 1, name: 'Pen', categoryId: 1});
+ }
+
+ function expectOptionsInjectedFromCategory() {
+ expect(actualOptions).to.have.property('injectedFrom', 'Category');
+ }
+ });
+
+ function setupAppAndRequest() {
+ app = loopback({localRegistry: true});
+ app.dataSource('db', {connector: 'memory'});
+
+ var settings = {
+ forceId: false,
+ replaceOnPUT: true,
+ injectOptionsFromRemoteContext: true,
+ };
+
+ Product = app.registry.createModel(
+ 'Product',
+ {name: String},
+ settings);
+
+ Product.createOptionsFromRemotingContext = function(ctx) {
+ return {injectedFrom: 'Product'};
+ };
+
+ app.model(Product, {dataSource: 'db'});
+
+ app.use(loopback.rest());
+ request = supertest(app);
+ }
+
+ function resetActualOptions() {
+ actualOptions = undefined;
+ }
+
+ function observeOptionsBeforeSave() {
+ var Model = arguments[0] || Product;
+ Model.observe('before save', function(ctx, next) {
+ actualOptions = ctx.options;
+ next();
+ });
+ }
+
+ function observeOptionsBeforeDelete() {
+ var Model = arguments[0] || Product;
+ Model.observe('before delete', function(ctx, next) {
+ actualOptions = ctx.options;
+ next();
+ });
+ }
+
+ function observeOptionsOnAccess() {
+ var Model = arguments[0] || Product;
+ Model.observe('access', function(ctx, next) {
+ actualOptions = ctx.options;
+ next();
+ });
+ }
+
+ function givenProductId1() {
+ return Product.create({id: 1, name: 'Pen'});
+ }
+
+ function expectInjectedOptions(name) {
+ expect(actualOptions).to.have.property('injectedFrom');
+ }
+});
diff --git a/test/data-source.test.js b/test/data-source.test.js
index 662c07184..a3ec55ef0 100644
--- a/test/data-source.test.js
+++ b/test/data-source.test.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
describe('DataSource', function() {
var memory;
@@ -17,6 +22,7 @@ describe('DataSource', function() {
assert.isFunc(Color, 'findOne');
assert.isFunc(Color, 'create');
assert.isFunc(Color, 'updateOrCreate');
+ assert.isFunc(Color, 'upsertWithWhere');
assert.isFunc(Color, 'upsert');
assert.isFunc(Color, 'findOrCreate');
assert.isFunc(Color, 'exists');
@@ -78,6 +84,7 @@ describe('DataSource', function() {
existsAndShared('_forDB', false);
existsAndShared('create', true);
existsAndShared('updateOrCreate', true);
+ existsAndShared('upsertWithWhere', true);
existsAndShared('upsert', true);
existsAndShared('findOrCreate', false);
existsAndShared('exists', true);
diff --git a/test/e2e/remote-connector.e2e.js b/test/e2e/remote-connector.e2e.js
index 1a3424718..e24436929 100644
--- a/test/e2e/remote-connector.e2e.js
+++ b/test/e2e/remote-connector.e2e.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var path = require('path');
var loopback = require('../../');
var models = require('../fixtures/e2e/models');
@@ -19,7 +24,9 @@ describe('RemoteConnector', function() {
foo: 'bar'
}, function(err, inst) {
if (err) return done(err);
+
assert(inst.id);
+
done();
});
});
@@ -30,7 +37,9 @@ describe('RemoteConnector', function() {
});
m.save(function(err, data) {
if (err) return done(err);
+
assert(data.foo === 'bar');
+
done();
});
});
diff --git a/test/e2e/replication.e2e.js b/test/e2e/replication.e2e.js
index 24f6967e0..ad7d57363 100644
--- a/test/e2e/replication.e2e.js
+++ b/test/e2e/replication.e2e.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2014,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var path = require('path');
var loopback = require('../../');
var models = require('../fixtures/e2e/models');
@@ -27,8 +32,10 @@ describe('Replication', function() {
}, function(err, created) {
LocalTestModel.replicate(0, TestModel, function() {
if (err) return done(err);
- TestModel.findOne({n: RANDOM}, function(err, found) {
+
+ TestModel.findOne({ n: RANDOM }, function(err, found) {
assert.equal(created.id, found.id);
+
done();
});
});
diff --git a/test/email.test.js b/test/email.test.js
index 018f543ca..c1303d8db 100644
--- a/test/email.test.js
+++ b/test/email.test.js
@@ -1,3 +1,8 @@
+// Copyright IBM Corp. 2013,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../');
var MyEmail;
var assert = require('assert');
@@ -33,6 +38,14 @@ describe('Email connector', function() {
assert(connector.transportForName('smtp'));
});
+ it('should set up a aliased transport for SMTP' , function() {
+ var connector = new MailConnector({transport:
+ {type: 'smtp', service: 'ses-us-east-1', alias: 'ses-smtp'}
+ });
+
+ assert(connector.transportForName('ses-smtp'));
+ });
+
});
describe('Email and SMTP', function() {
@@ -61,6 +74,7 @@ describe('Email and SMTP', function() {
assert(mail.response);
assert(mail.envelope);
assert(mail.messageId);
+
done(err);
});
});
@@ -78,6 +92,7 @@ describe('Email and SMTP', function() {
assert(mail.response);
assert(mail.envelope);
assert(mail.messageId);
+
done(err);
});
});
diff --git a/test/error-handler.test.js b/test/error-handler.test.js
index d19abf47e..d522c5e6c 100644
--- a/test/error-handler.test.js
+++ b/test/error-handler.test.js
@@ -1,6 +1,11 @@
+// Copyright IBM Corp. 2015,2016. All Rights Reserved.
+// Node module: loopback
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
var loopback = require('../');
var app;
-var assert = require('assert');
+var expect = require('chai').expect;
var request = require('supertest');
describe('loopback.errorHandler(options)', function() {
@@ -16,7 +21,9 @@ describe('loopback.errorHandler(options)', function() {
request(app)
.get('/url-does-not-exist')
.end(function(err, res) {
- assert.ok(res.error.text.match(/