Compare commits

...

279 Commits

Author SHA1 Message Date
Miroslav Bajtoš 6638992b99
2.41.1
* Fix: treat empty access token string as undefined (andrey-abramow)
 * Fix context propagation broken by async@2.x (Miroslav Bajtoš)
2018-11-26 11:14:37 +01:00
Miroslav Bajtoš e0fc5139c7
Merge pull request #4079 from andrey-abramow/2.x
Fix compatibility with loopback-datasource-juggler 2.56.0
2018-11-26 11:14:02 +01:00
andrey-abramow 21e69f0c14
Fix: treat empty access token string as undefined
Fix AccessToken's method tokenIdForRequest to treat an empty string
as if no access token was provided.

This is needed to accomodate the changes made in
loopback-datasource-juggler@2.56.0.
2018-11-26 11:00:02 +01:00
Miroslav Bajtoš b064b6d4bf
Merge pull request #4025 from strongloop/disable-context-tests
Fix context propagation broken by async@2.x
2018-10-15 16:44:55 +02:00
Miroslav Bajtoš 228bc7519b
Fix context propagation broken by async@2.x
Rework the REST middleware to use a hand-written version of
"async.eachSeries". Before this change, we were loosing CLS context
when the application was relying on the REST middleware to load the
context middleware.

This is fixing a problem introduced by post-1.0 versions of async,
which we upgraded to via fea3b781a.
2018-10-15 13:30:20 +02:00
Diana Lau 43a1f537db 2.41.0
* Update LB2 LTS version (Diana Lau)
2018-10-09 16:06:34 -07:00
Diana Lau 24d0338941
Merge pull request #4023 from strongloop/2.x-status
Update LB2 LTS version
2018-10-09 18:58:25 -04:00
Diana Lau 087dae6a13 Update LB2 LTS version 2018-10-05 22:36:09 -04:00
virkt25 b27971e074 2.40.0
* fix: accessToken create default acl (virkt25)
2018-08-08 18:17:11 -04:00
virkt25 c36f9e88a3 fix: accessToken create default acl 2018-08-08 18:15:09 -04:00
Kevin Delisle 8be91b8129 2.39.2
* Babelify juggler for Karma tests (Miroslav Bajtoš)
 * Fix Karma config to babelify node_modules too (Miroslav Bajtoš)
2018-02-12 12:03:02 -05:00
Kevin Delisle 490eced414
Merge pull request #3790 from strongloop/backport/babel-es6-to-es5
Backport/babel es6 to es5
2018-02-12 12:01:32 -05:00
Miroslav Bajtoš 1575becb92 Babelify juggler for Karma tests
Fix configuration of Karma:

 - Disable ES6 modules. The ES6 module transpiler is adding
  "use strict" to all source files, this breaks e.g. chai or juggler
 - Relax "ignore" setting to exclude only strong-task-emitter,
   thus bring back Babel transpilation for chai and juggler.
2018-02-09 10:38:22 -05:00
Miroslav Bajtoš b2cf877d14 Fix Karma config to babelify node_modules too
Before this change, dependencies in node_modules (e.g. strong-remoting)
were not transformed to ES5 and thus crashed the tests in PhantomJS.

Note that loopback-datasource-juggler cannot be babelified to ES5
because it does not correctly support strict mode yet.
2018-02-08 16:58:12 -05:00
Taranveer Virk c650b0db87 2.39.1
* update juggler dep (Taranveer Virk)
 * fix(id): replace with != null (Samuel Reed)
 * fix(AccessContext): Tighten userid/appid checks (Samuel Reed)
2018-01-31 16:58:02 -05:00
Taranveer Virk 9e30e277fe
Merge pull request #3782 from strongloop/update-juggler-2
update juggler dep
2018-01-31 16:41:07 -05:00
Taranveer Virk 7ddc0b14cf update juggler dep 2018-01-31 14:40:37 -05:00
Miroslav Bajtoš 6425e8307e
Merge pull request #3719 from STRML/fix/falsy-id
fix(id): replace with != null
2017-12-08 15:25:12 +01:00
Samuel Reed 2e0f3d15f9
fix(id): replace with != null
Ref: #2356, #2374, #3130, #3693
2017-12-05 09:49:50 -06:00
Miroslav Bajtoš e22b960d4e
Merge pull request #3693 from STRML/fix/falsy-principalid-2x
fix(AccessContext): Tighten falsy userid/appid check
2017-12-05 16:20:34 +01:00
Samuel Reed 787f393c7c
fix(AccessContext): Tighten userid/appid checks
An application may have a use for a falsy ID.
2017-12-05 08:51:21 -06:00
Miroslav Bajtoš 6e0e60c2a2
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-10-23 09:22:09 +02:00
Miroslav Bajtoš 01e2e61cfe Merge pull request #3660 from strongloop/drop/node-0x
Drop support for Node.js versions 0.10 and 0.12
2017-10-23 09:20:37 +02:00
Miroslav Bajtoš 538bc9a7d5
Drop support for Node.js versions 0.10 and 0.12
Some of our dependencies are no longer supporting pre-4.0 versions of
Node.js. As a result, our CI builds are failing on these platforms.

This pull request removes 0.10 and 0.12 from our Travis CI build matrix
and also adds "engines" field to package.json to tell our internal
Jenkins CI to stop testing 0.10 and 0.12 versions too.
2017-10-19 16:47:53 +02:00
Miroslav Bajtoš 566caa9bab Merge pull request #3655 from strongloop/fix/build-2x
test: fix too strict test assertion
2017-10-19 16:47:01 +02:00
Miroslav Bajtoš 4f928bf965
test: fix too strict test assertion
Rework the test verifying properties of `loopback` to ignore
new express properties added after the test was written.

Ignore "json" and "urlencoded" middleware that was added back
to Express, keep using our wrappers printing a deprecation message.
2017-10-17 14:16:20 +02:00
Loay cd8f1775bc Merge pull request #3481 from strongloop/empty_password
Add unit test for empty password
2017-07-21 16:52:39 -04:00
loay 22bd0fc81f Add unit test for empty password 2017-07-21 15:46:21 -04:00
Candy 0fdca8ee0d Merge pull request #3419 from kallenboone/piiReturn_2.x
Update translated strings [PII Return Q2 2017]
2017-05-23 14:43:14 -04:00
Allen Boone 41c31118d8 Update translated strings Q2 2017 2017-05-23 13:02:39 -04:00
Candy 2135abc1db 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-04-17 16:40:50 -04:00
Diana Lau 661d62e817 Merge pull request #3346 from strongloop/update-dependencies
[2.x] Update deprecated dependencies
2017-04-17 18:03:29 +00:00
Diana Lau f1f9aab606 use lower version of karma-browserify 2017-04-13 15:28:48 -04:00
Diana Lau 645d5c615b update karma-browserify to 5.x
update karma-browserify to 5.x
2017-04-12 10:43:22 -04:00
Diana Lau 62d6ecb9d2 Merge pull request #3354 from strongloop/update-msg
[2.x] update translation msg
2017-04-12 14:07:02 +00:00
Diana Lau 1ec7a265a7 update translation msg 2017-04-11 14:36:41 -04:00
Miroslav Bajtoš cf38c62c00 Merge pull request #3330 from strongloop/fix/unauthorized-current-user-literal-2x
Fix user-literal rewrite for anonymous requests
2017-04-04 19:26:45 +02:00
Aaron Buchanan 50e0e4808a
Fix user-literal rewrite for anonymous requests
Currently any `currentUserLiteral` routes when accessed with a bad
token throw a 500 due to a SQL error that is raised because
`Model.findById` is invoked with `id={currentUserLiteral}`
(`id=me` in our case) when the url rewrite fails.

This commit changes the token middleware to return 401 Not Authorized
when the client is requesting a currentUserLiteral route without
a valid access token.
2017-04-04 18:55:34 +02:00
Miroslav Bajtoš ac2462e11d Merge pull request #3309 from strongloop/fix/options-in-token-invalidations
Forward options in prepareForTokenInvalidation
2017-03-28 15:46:59 +02:00
Miroslav Bajtoš a5ac1506e6
Forward options in prepareForTokenInvalidation 2017-03-24 15:01:05 +01:00
Raymond Feng 78161ccd9b 2.38.2
* Fix file patch (Raymond Feng)
 * Add nyc coverage, report data to coveralls.io (Miroslav Bajtoš)
2017-03-17 08:54:26 -07:00
Raymond Feng cfb0148e53 Fix file patch 2017-03-17 08:46:35 -07:00
Miroslav Bajtoš 85c81f760c Merge pull request #3280 from strongloop/coveralls-2x
Add nyc coverage, report data to coveralls.io
2017-03-14 14:20:40 +01:00
Miroslav Bajtoš 4713e5e7ea
Add nyc coverage, report data to coveralls.io 2017-03-14 13:42:15 +01:00
Miroslav Bajtoš 45284c3bf9
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-03-13 16:22:17 +01:00
Miroslav Bajtoš 989c3bba1f Merge pull request #3259 from strongloop/backport/fix-verifyHref-uid
Fix User.verify to convert uid to string
2017-03-09 08:57:02 +01:00
phairow 91502db9f1
Fix User.verify to convert uid to string
Applications using MongoDB connectors typically have `user.id`
property of type ObjectID.

This commit fixes the code building the verification URL to
correctly convert the user id value into string.
2017-03-08 16:30:01 +01:00
Miroslav Bajtoš 10fddb64f7 Merge pull request #3222 from strongloop/backport/cache-phantomjs-on-travis
Configure Travis CI to cache phantomjs binaries [2.x]
2017-02-23 11:52:48 +01:00
Miroslav Bajtoš e334884fb1
Configure Travis CI to cache phantomjs binaries
This should speed up our CI builds and also save a lot of bandwidth
for people providing phantomjs-prebuilt module.

See also
https://www.npmjs.com/package/phantomjs-prebuilt#continuous-integration
2017-02-22 15:34:28 +01:00
Miroslav Bajtoš 68d55b523e Merge pull request #3219 from strongloop/backport/fix-filter-desc
Improve "filter" arg description
2017-02-21 17:28:51 +01:00
Raymond Camden a4154caf59
Improve "filter" arg description
Add an example showing how to serialize object values as JSON.
2017-02-21 15:00:57 +01:00
Miroslav Bajtoš 42780567a8 Merge pull request #3195 from strongloop/backport/fix-hash-path-in-redirect
Fix creation of verification links
2017-02-09 15:20:52 +01:00
Miroslav Bajtoš 09b1fce34b Fix creation of verification links
Fix User.prototype.verify to call `querystring.stringify` instead
of concatenating query-string components directly.

In particular, this fixes the bug where `options.redirect` containing
a hash fragment like `#/home?arg1=value1&arg2=value2` produced incorrect
URL, because the `redirect` value was not correctly encoded.
2017-02-09 13:33:57 +01:00
Miroslav Bajtoš 4e8252afad Merge pull request #3193 from strongloop/add-doc-url-for-logoutSession
Include link to docs in logoutSessions warning
2017-02-09 13:30:12 +01:00
Miroslav Bajtoš 8c76d7fc01 Include link to docs in logoutSessions warning 2017-02-08 09:02:39 +01:00
Miroslav Bajtoš 322f6c12bb Merge pull request #3167 from strongloop/backport/fix-token-invalidation-on-save
Preserve sessions on User.save() making no changes
2017-01-31 16:46:09 +01:00
Miroslav Bajtoš 0cc2b5b8db Fix detection of logoutSessionsOnSensitiveChanges
Modify the code detecting whether logoutSessionsOnSensitiveChanges
is enabled to correctly handle the case when the model is not attached
to any application, as is the case with loopback-component-passport
tests.
2017-01-31 15:53:41 +01:00
Miroslav Bajtoš 05db4337cf Preserve sessions on User.save() making no changes 2017-01-31 14:43:24 +01:00
Miroslav Bajtoš 50743e94be Merge pull request #3162 from strongloop/remove-unused-deps
Remove unused dependencies
2017-01-31 09:29:15 +01:00
Miroslav Bajtoš 6a4198896f Remove unused dependencies
- strong-error-handler
 - eslint

These dependencies were most likely added accidentally by fea3b781.
2017-01-30 15:09:59 +01:00
Miroslav Bajtoš 03391f7b00 Merge pull request #3158 from strongloop/backport/fix-logout-without-token
Fix logout to handle no or missing accessToken
2017-01-30 11:03:39 +01:00
Ritchie Martori 1dac9ada0b Fix logout to handle no or missing accessToken
Return 401 when the request does not provide any accessToken argument
or the token was not found.

Also simplify the implementation of the `logout` method to make only
a single database call (`deleteById`) instead of `findById` + `delete`.
2017-01-30 10:39:40 +01:00
Miroslav Bajtoš 2ade55ec03 Merge pull request #3148 from strongloop/backport/fix-language-in-tests
Use English when running Mocha tests
2017-01-27 13:10:49 +01:00
Miroslav Bajtoš 98110f1b84 Use English when running Mocha tests 2017-01-27 11:26:24 +01:00
Miroslav Bajtoš 97f96f4ab8 Merge pull request #3135 from strongloop/backport/fix-role-models-resolution
Role model: resolve related models by name
2017-01-25 16:23:48 +01:00
Benjamin Kroeger 56ad85ae2a Role model: resolves related models by name
Resolve models related to the `Role` model by name instead of class.
2017-01-25 11:02:55 +01:00
Miroslav Bajtoš b2a00286a3 Merge pull request #3129 from strongloop/backport/fix-user-id
Fix User methods to use correct Primary Key
2017-01-23 10:59:11 +01:00
Aris Kemper 5e7e7ca7e9 Fix User methods to use correct Primary Key
Do not use hard-coded "id" property name, call `idName()` to get the
name of the PK property.
2017-01-23 09:51:41 +01:00
Miroslav Bajtoš 6fcb7dba6a 2.38.0
* Add app setting logoutSessionsOnSensitiveChanges (Miroslav Bajtoš)
 * Fix User.resetPassword to call createAccessToken() (João Ribeiro)
2017-01-20 15:10:26 +01:00
Miroslav Bajtoš b541c5bff8 Merge pull request #3109 from strongloop/fix/flag-to-invalidate-tokens
Add app setting logoutSessionsOnSensitiveChanges
2017-01-20 15:09:08 +01:00
Miroslav Bajtoš f1e31ca50c Add app setting logoutSessionsOnSensitiveChanges
Disable invalidation of access tokens by default to restore backwards
compatibility with older 2.x versions.

Add a new application-wide flag logoutSessionsOnSensitiveChanges
that can be used to explicitly turn on/off the token invalidation.

When the flag is not set, a verbose warning is printed to nudge the user
to make a decision how they want to handle token invalidation.
2017-01-20 12:57:23 +01:00
Miroslav Bajtoš f355f66114 Merge pull request #3123 from strongloop/backport/fix-user-reset-password
Fix User.resetPassword to call createAccessToken()
2017-01-20 12:55:21 +01:00
João Ribeiro b8f9b85609 Fix User.resetPassword to call createAccessToken()
This allows User subclasses to override the algorithm used for building
one-time access tokens for password recovery.
2017-01-20 10:59:46 +01:00
Miroslav Bajtoš d35e1a1b6f 2.37.1
* Preserve current session when invalidating tokens (Miroslav Bajtoš)
 * Clean up access-token-invalidation tests (Miroslav Bajtoš)
2017-01-16 12:00:57 +01:00
Miroslav Bajtoš c7d07b6600 Merge pull request #3103 from strongloop/backport/preserve-current-access-token
Preserve current session when invalidating tokens
2017-01-16 12:00:08 +01:00
Miroslav Bajtoš afd6dd7073 Preserve current session when invalidating tokens
Fix User model to preserve the current session (provided via
"options.accessToken") when invalidating access tokens after a change
of email or password property.
2017-01-16 11:02:29 +01:00
Miroslav Bajtoš f8b013dab8 Clean up access-token-invalidation tests 2017-01-16 10:39:49 +01:00
Miroslav Bajtoš dc2b6530b7 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)
2017-01-09 12:58:30 +01:00
Miroslav Bajtoš f64721a447 Merge pull request #3073 from strongloop/backport/resetPasswordRequest-options
Emit resetPasswordRequest event with options
2017-01-05 16:00:09 +01:00
Sergey Reus 5233dcb557 Emit resetPasswordRequest event with options 2017-01-05 15:36:01 +01:00
Miroslav Bajtoš 0caee53f6b Merge pull request #3072 from strongloop/backport/email-verified-fix
Fix false emailVerified on user model update
2017-01-05 11:38:16 +01:00
박대선 659e9ce09b Fix false emailVerified on user model update
We noticed that every time the user model updates, the emailVerified
column would change to false, even though the email was not changed
at all.

I took a look and realized there might be an error in
https://github.com/strongloop/loopback/commit/eb640d8

The intent of the commit just mention is to make emailVerified false
when the email gets changed, but notice that ctx.data.email is null
on updates, so the condition is always met and emailVerified always
becomes false.

This commit fixes the issue just mentioned.
2017-01-05 11:08:35 +01:00
Miroslav Bajtoš 6e3fc24121 Merge pull request #3048 from strongloop/backport/options-from-context-2x
Inject remoting context to options arg
2017-01-05 10:58:46 +01:00
Miroslav Bajtoš 74bb1daf8a Add new flag injectOptionsFromRemoteContext
Hide the new "options" arguments behind a feature flag
injectOptionsFromRemoteContext that is disabled by default for backwards
compatibility.

Fix construction of sharedCtor remoting metadata to prevent the
situation when we are configuring remoting metadata after
strong-remoting has already picked up data from our parent (base) model.
2017-01-05 10:18:56 +01:00
Miroslav Bajtoš 693d52fc59 Contextify DAO and relation methods
Modify remoting metadata of data-access methods in PersistedModel
and relation method in Model and add an "options" argument to "accepts"
list.
2017-01-05 10:18:56 +01:00
Miroslav Bajtoš ee106e4e15 Implement new http arg mapping optionsFromRequest
Define a new Model method "createOptionsFromRemotingContext" that allows
models to define what "options" should be passed to methods invoked
via strong-remoting (e.g. REST).

Define a new http mapping `http: 'optionsFromRequest'` that invokes
`Model.createOptionsFromRemotingContext` to build the value from
remoting context.

This should provide enough infrastructure for components and
applications to implement their own ways of building the "options"
object.
2017-01-05 10:18:56 +01:00
David Cheung 65a3a0b110 Merge pull request #3067 from strongloop/fix-packagejson-wrong-ci-config
Fix package.json CI downstreamIgnoreList nesting
2017-01-03 17:05:40 -05:00
David Cheung d53d069763 Fix package.json CI downstreamIgnoreList nesting
in packge.json strongloop/loopback#3000 ci should be a root element
instead of under config:ci
2017-01-03 15:23:57 -05:00
Simon Ho 9c3d596106 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-12-21 17:54:34 -08:00
Miroslav Bajtoš 18a89e556a Merge pull request #2960 from kobaska/avoid-cleanup
Avoid periodic cleanup/rectification of changes
2016-12-21 16:13:27 +01:00
kobaska b3a5bc739b Add option disabling periodic change rectification
When `Model.settings.changeCleanupInterval` is set to a negative value,
no periodic cleanup is performed at all.
2016-12-21 15:39:08 +01:00
Simon Ho fe1c0b605b Release LTS LB2 2016-12-20 11:32:12 -08:00
Miroslav Bajtoš 5200b28deb Merge pull request #3021 from strongloop/fix/session-expiry-2x
Invalidate AccessTokens on password change
2016-12-12 14:59:16 +01:00
Miroslav Bajtoš 4ee086dcd0 Invalidate AccessTokens on password change
Invalidate all existing sessions (delete all access tokens)
after user's password was changed.
2016-12-12 13:58:20 +01:00
Miroslav Bajtoš 66e4e5be4a Merge pull request #3015 from strongloop/fix/repeated-user-hooks-2x
Fix registration of operation hooks in User model [2.x]
2016-12-09 14:52:06 +01:00
Miroslav Bajtoš 01b2faf14a Fix registration of operation hooks in User model
Operation hooks are inherited by subclassed models, therefore they must
be registered outside of `Model.setup()` function.

This commit fixes this problem in the built-in User model.

There are not tests verifying this change, as writing a test would be
too cumbersome and not worth the cost IMO.
2016-12-09 14:21:38 +01:00
Miroslav Bajtoš 9bea50c5e2 Merge pull request #3007 from strongloop/backport/email-template-in-transport
Remove "options.template" from Email payload
2016-12-07 12:32:11 +01:00
Miroslav Bajtoš 4d41c67c54 Remove "options.template" from Email payload
Fix User.confirm to exclude "options.template" when sending the
confirmation email. Certain nodemailer transport plugins are rejecting
such requests.
2016-12-07 10:54:17 +01:00
David Cheung 956f035482 Merge pull request #3000 from strongloop/opt-out-broken-downstream-2.x
Opt-out downstream builds that are unstable
2016-12-06 14:08:48 -05:00
David Cheung a759286330 Opt-out downstream builds that are unstable
repos that are opting out are not a good indicator of stability of
this module, and are failing
2016-12-06 10:35:13 -05:00
David Cheung 1fa785f66f Merge pull request #2980 from fullcube/bb/password-reset-realms
Allow password reset request for users in realms
2016-12-05 15:26:09 -05:00
Bram Borggreve e7831f6c4d
Allow password reset request for users in realms 2016-11-30 16:57:59 -05:00
Miroslav Bajtoš 63df861753 Merge pull request #2993 from strongloop/backport/returnOnlyRoleNames
Add "returnOnlyRoleNames" option to Role.getRoles
2016-11-30 17:23:20 +01:00
Eric a4a96eb39f Add "returnOnlyRoleNames" option to Role.getRoles
Currently the return type of Role.getRoles() method is inconsistent:
role names are returned for smart roles and role ids are returned for
static roles (configured through user-role mapping).

This commit adds a new option to Role.getRoles() allowing the caller
to request role names to be returned for all types of roles.
2016-11-30 17:10:41 +01:00
Miroslav Bajtoš c0e96ffa12 Merge pull request #2940 from kobaska/add-optional-options-to-bulkupdate
Add options to bulkUpdate
2016-11-15 17:34:06 +01:00
Miroslav Bajtoš 007b20df0c Merge pull request #2944 from strongloop/feature/role-context-2x
Fix context within listByPrincipalType role method
2016-11-15 16:59:35 +01:00
codyolsen d99d608876 Fix context within listByPrincipalType role method
- Fix for current implimentation that returned all models that had any
  assigned roles. Context was not carried into listByPrincipalType,
  setting roleId as null.
2016-11-15 16:27:56 +01:00
Miroslav Bajtoš 586fa1cebb Merge pull request #2938 from strongloop/feature/verify-template-fn-2x
Add templateFn option to User#verify()
2016-11-15 14:17:32 +01:00
Adrien Kiren 5c1558f969 Add templateFn option to User#verify() 2016-11-15 13:46:35 +01:00
Kogulan Baskaran bc923bd781 Add options to bulkUpdate 2016-11-15 13:02:23 +11:00
Loay d06190dae6 Merge pull request #2927 from strongloop/backport/require-verification-email-change
Backport/Require verification after email change
2016-11-09 13:09:33 -05:00
Loay 67e5c6ec1e Require verification after email change
When the User model is configured to require email verification,
then any change of the email address should trigger re-verification.
2016-11-09 11:51:37 -05:00
Loay d61e173d6f Merge pull request #2914 from strongloop/backport-email-case-sensitive
[backport #2912] adding check of string for case insensitive emails
2016-11-04 23:25:01 -04:00
Dhaval Trivedi 6e880137e4 adding check of string for case insensitive emails 2016-11-04 10:35:08 -04:00
Simon Ho 1915d09424 Merge pull request #2890 from strongloop/backport/fix/pr-template
Fix PR template to not link all PRs to #49 (#2887)
2016-10-24 17:44:31 -07:00
Miroslav Bajtoš f80b27880e Fix PR template to not link all PRs to #49 (#2887)
- Add comment with syntax examples
- Remove direct links to #49 in section header

Backport of #2887
2016-10-24 17:10:02 -07:00
Miroslav Bajtoš 4cb9f0d74d 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-24 10:40:26 +02:00
Simon Ho 3e0fd94f60 Need index on principalId for performance. (#2883) (#2884)
Backport of #2883
2016-10-21 16:31:33 -07:00
Simon Ho df13b094bb Remove redundant items in PR template (#2877) (#2878)
- Remove sign CLA since CI already shows unsigned CLAs
- Remove all tests must pass CI since we have to check each on a case by
  case basis anyways
- Update example to include linking to the current repo using number
  sign (ie. #49 in addition to org/repo#49)

Backport of #2877
2016-10-19 18:25:04 -07:00
Simon Ho 809ba35fdb Refactor PR template based on feedback (#2865) (#2874)
Updated based on feedback received during sprint demo.

Backport of #2865
2016-10-18 23:53:43 -07:00
Simon Ho f97906d397 Add pull request template (#2843) (#2862)
Backport of #2843
2016-10-17 09:02:24 -07:00
Miroslav Bajtoš f9cd880eaa Merge pull request #2846 from strongloop/fix/metadata-update-delete-all-2x
Fix description of updateAll response
2016-10-17 14:10:46 +02:00
Miroslav Bajtoš 060630aad6 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-10-13 10:12:11 +02:00
Simon Ho 446d2a5078 Reword ticking checkbox note in issue template (#2855)
Backport of #2854
2016-10-12 18:03:09 -07:00
Simon Ho 81c951f212 Add how to tick checkbox in issue template (#2851) (#2853)
Many people are putting "*" instead of x, which causes the markdown to
render funny. Adding instructions to that particular section.

Backport of #2851
2016-10-12 17:27:22 -07:00
Simon Ho f72fb69cfe Use GitHub issue templates (#2810) (#2852)
Backport of #2810
2016-10-12 17:08:56 -07:00
Miroslav Bajtoš bf5c206bd6 Fix description of updateAll response
Correctly describe the first non-error callback arg as an `info` object
containing a `count` property.
2016-10-12 13:04:35 +02:00
Miroslav Bajtoš 765e53098b Merge pull request #2845 from strongloop/feature/allow-eternal-access-tokens-2x
Allow tokens with eternal TTL (value -1)
2016-10-12 12:55:12 +02:00
Miroslav Bajtoš b3497c6778 Allow tokens with eternal TTL (value -1)
- Add a new User setting 'allowEternalTokens'
 - Enhance 'AccessToken.validate' to support eternal tokens with ttl
   value -1 when the user model allows it.
2016-10-12 12:30:33 +02:00
Candy b445d80191 Merge pull request #2836 from strongloop/add_translation3_2x
Update ja and nl translation files
2016-10-06 15:27:06 -04:00
Candy 6d08df4f0a Update ja and nl translation files 2016-10-06 14:19:57 -04:00
Miroslav Bajtoš 87a89db15f Merge pull request #2822 from strongloop/fix/promise-remote-hooks-2x
Fix support for remote hooks returning a Promise
2016-10-05 13:59:10 +02:00
Tim van der Staaij b8b92fbeda Fix support for remote hooks returning a Promise
Fix beforeRemote/afterRemote to correctly return promises returned
by the user-provided hook callback.
2016-10-05 11:02:36 +02:00
Loay 715bc1ece6 Merge pull request #2814 from strongloop/backport/partial-update
Validate non-email property partial update
2016-10-03 18:02:47 -04:00
Loay bdeaf654fa Validate non-email property partial update 2016-10-03 16:53:36 -04:00
Amirali Jafarian 46f0c061c0 Merge pull request #2804 from strongloop/add_translation2_2x
Update translation files - round#2
2016-09-28 17:25:56 -04:00
Candy 26147b6bc2 Update translation files - round#2 2016-09-28 15:32:12 -04:00
Gunjan Pandya 7037994a6b Merge pull request #2782 from strongloop/backport/compat_flag_cleanup
Update tests to use registry for model creation [2.x]
2016-09-27 18:49:55 -04:00
gunjpan e244153eb7 Update tests to use registry for model creation
Current implementation of `app.model(modelName, settings)`
works as a sugar for model creation. In LB 3.0, this is
not supported anymore. This backporting:
- keeps the sugar method for model creation  for backward
compatibility
- updates test cases to use `app.registry.createModel()`
for model creation

Backport of #2401
2016-09-27 16:44:01 -04:00
Richard Pringle 4a0835a032 Merge pull request #2781 from strongloop/backport/disableMethodByName
Call new disable remote method from model class [2.x]
2016-09-26 10:59:01 -04:00
Richard Pringle f7dbc97763 Call new disable remote method from model class. 2016-09-23 11:14:51 -04:00
Miroslav Bajtoš 5ee43fec83 Merge pull request #2783 from strongloop/fix/windows-ci-2x
Temporarily disable Karma tests on Windows CI
2016-09-23 10:22:06 +02:00
Miroslav Bajtoš 6ac1f694b9 Temporarily disable Karma tests on Windows CI
We are observing frequent test failures on Windows CI, where PhantomJS
cannot start because there are no free handles available. Finding
and fixing the process leaking handles is non-trivial and will take
long time.

This commit disables Karma tests on Windows CI machines to prevent
build failures that we are ignoring anyways.
2016-09-23 09:58:16 +02:00
Candy f3317ec39b Merge pull request #2768 from strongloop/add_translation
Add translation files for 2.x
2016-09-21 17:34:45 -04:00
Candy 74063ab559 Add translation files for 2.x 2016-09-20 14:15:19 -04:00
Loay bd3f875ced Merge pull request #2763 from strongloop/backport/allow-reset-emailVerified
Backport/ Allow resetPassword if EmailVerified
2016-09-20 13:03:08 -04:00
Loay 59eeb99803 Allow resetPassword if email is verified 2016-09-20 11:29:56 -04:00
Loay e10dcf7c2c Merge pull request #2759 from strongloop/backport/invalidate-email-sessions
Backport/invalidate email sessions
2016-09-20 09:48:16 -04:00
Simon Ho f7c74eb24d Merge pull request #2760 from strongloop/backport/docs-for-kv-model
Backport: Add docs for KeyValue model
2016-09-19 17:08:26 -07:00
Simon Ho f7f448d569 Add docs for KeyValue model
Backport of #2743
2016-09-19 15:39:01 -07:00
Loay fa310d5882 Invalidate sessions after email change 2016-09-19 14:55:23 -04:00
Miroslav Bajtoš 14b8426687 Merge pull request #2736 from strongloop/update/loopback-testing-in-2x
Upgrade loopback-testing to the latest ^1.4 [2.x-only]
2016-09-13 09:13:57 +02:00
Miroslav Bajtoš ec8250cf58 2.34.1
* Fix double-slash in confirmation URL (Miroslav Bajtoš)
2016-09-13 09:08:28 +02:00
Miroslav Bajtoš 68558d7afa Merge pull request #2718 from strongloop/fix/user-verify-email-with-empty-rest-root-2x
Fix double-slash in confirmation URL
2016-09-13 08:49:23 +02:00
Miroslav Bajtoš 3df5b2814c Fix double-slash in confirmation URL
Fix the code building the URL used in the email-verification email
to prevent double-slash in the URL when e.g. restApiRoot is '/'.

Before:

  http://example.com//users/confirm?...

Now:

  http://example.com/users/confirm?...
2016-09-12 17:12:24 +02:00
Miroslav Bajtoš c4214024be Upgrade loopback-testing to the latest ^1.4 2016-09-12 13:21:07 +02:00
Miroslav Bajtoš 7d1f31cfb4 2.34.0 2016-09-12 11:27:35 +02:00
Miroslav Bajtoš 8f642b593c 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-09 10:25:18 +02:00
Amirali Jafarian 73962fb3cc Merge pull request #2729 from strongloop/Fix_data_argument_upsertWithWhere
Fix data argument for upsertWithWhere
2016-09-07 14:19:27 -04:00
Amir Jafarian 4d6f2da578 Fix data argument for upsertWithWhere
* Related PR: #2727
2016-09-07 13:52:35 -04:00
Amirali Jafarian f741c1c3b9 Merge pull request #2722 from strongloop/expose_upsertWithWhere_2.x
Expose upsertWithWhere
2016-09-07 13:11:58 -04:00
Sonali Samantaray 4c013deaae Expose upsertWithWhere
Backport of #2539
2016-09-07 12:41:56 -04:00
Miroslav Bajtoš 69103d53f1 Merge pull request #2702 from strongloop/fix/data-object-arguments
Fix remoting metadata for "data" arguments
2016-09-07 14:09:24 +02:00
Miroslav Bajtoš fcfdb73bdb Fix remoting metadata for "data" arguments
Fix the definition of "data" argument to

    { type: 'object', model: modelName, ... }

That way strong-remoting passed the request body directly to the model
method (does not create a new model instance), but the swagger will
still provide correct schema for these arguments.

This fixes a bug where upsert in relation methods was adding default
property values to request payload.
2016-09-07 12:59:01 +02:00
Miroslav Bajtoš 74deec8142 Merge pull request #2716 from strongloop/feature/isemail-2x
Rework email validation to use isemail
2016-09-06 13:52:09 +02:00
Miroslav Bajtoš 381222bf7a Rework email validation to use isemail
Drop hand-crafted RegExp in favour of a 3rd-party module that supports
RFC5321, RFC5322 and other relevant standards.
2016-09-06 13:00:27 +02:00
Miroslav Bajtoš eb43412439 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-09-05 15:06:18 +02:00
Miroslav Bajtoš f362084770 Merge pull request #2697 from strongloop/fix/various
Fix app.enableAuth and test/user
2016-09-01 16:49:36 +02:00
Miroslav Bajtoš bc10d68c54 test/user: don't attach User model twice 2016-08-31 15:29:18 +02:00
Miroslav Bajtoš 07a04b71da app.enableAuth: correctly detect attached models
Fix a typo in "app.enableAuth" that caused the method to not detect
the situation when e.g. the built-in User model is already attached
to a datasource.
2016-08-31 15:29:18 +02:00
Candy c485d0f276 Merge pull request #2688 from strongloop/backport/fix_glob
Apply g.f to literal strings
2016-08-29 18:45:47 -04:00
Candy 069d3e8f2f Apply g.f to literal strings
Backport #2684
2016-08-29 10:40:36 -04:00
Miroslav Bajtoš 5f74e74ea0 Merge pull request #2687 from strongloop/backport/set-datasource-app-2x
Make the app instance available to connectors
2016-08-29 15:15:26 +02:00
Subramanian Krishnan 6e71a52e90 Make the app instance available to connectors 2016-08-29 14:57:43 +02:00
Amirali Jafarian bc3008d469 Merge pull request #2682 from strongloop/backport/reorder_put_patch
Reorder PATCH Vs PUT endpoints
2016-08-26 17:34:11 -04:00
Amir Jafarian 55eb8d72e6 Reorder PATCH Vs PUT endpoints
* Reorder PATCH Vs PUT endpoints for update* methods
* Backport of #2670
2016-08-26 16:21:45 -04:00
Miroslav Bajtoš f99e1a0242 Merge pull request #2673 from strongloop/fix/acl-related-model-resolution-2x
Fix acl related model resolution
2016-08-25 12:43:25 +02:00
Benjamin Kroeger ecd881a0f3 streamline use if `self` 2016-08-25 10:39:44 +02:00
Benjamin Kroeger c538aa764d resolve related models from correct registry
Also modify setup of test servers when ACL was used, force the app
to `loadBuiltinModels` with localRegistry.
2016-08-25 10:39:44 +02:00
Miroslav Bajtoš 0627f62e45 Merge pull request #2654 from strongloop/feature/kvao-iterate-keys-2x
KeyValueModel: add API for listing keys
2016-08-18 14:21:45 +02:00
Miroslav Bajtoš b221af7cf6 KeyValueModel: add API for listing keys
- Expose "keys()" at "GET /keys"
 - Add a dummy implementation for "iterateKeys" to serve a useful error
   message when the model is not attached correctly.
2016-08-18 13:34:29 +02:00
Miroslav Bajtoš 56fa9829f7 2.31.0
* Fix token middleware crash (Carl Fürstenberg)
 * Support 'alias' in mail transport config. (Samuel Reed)
2016-08-17 16:26:40 +02:00
Miroslav Bajtoš 91b9a00388 Merge pull request #2649 from strongloop/fix/token-in-context
Fix token middleware crash [2.x]
2016-08-17 14:19:35 +02:00
Carl Fürstenberg ba2fe0ee05 Fix token middleware crash
Fix token middleware to check if `req.loopbackContext` is active.
The context is not active for example when express-session calls
setImmediate which breaks CLS.
2016-08-17 13:42:43 +02:00
Miroslav Bajtoš 378aba60ae Merge pull request #2489 from STRML/mailAlias
Support 'alias' in mail transport config.
2016-08-16 16:19:45 +02:00
Miroslav Bajtoš 7f02191838 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-08-16 16:15:01 +02:00
Miroslav Bajtoš 86d0befa11 Merge pull request #2621 from strongloop/fix/unglobalize-swagger-2x
Revert globalization of Swagger descriptions [2.x]
2016-08-16 14:28:07 +02:00
Miroslav Bajtoš 7932d75c44 Revert globalization of Swagger descriptions 2016-08-16 14:02:41 +02:00
Amir-61 6c9df360b9 Merge pull request #2435 from strongloop/expose_endpoints_2.x
Expose `Replace*` methods for 2.x
2016-08-15 15:05:32 -04:00
Amir Jafarian e562137807 Expose `Replace*` methods
*Re-mapping `updateAttributes` endpoint to use
`PATCH` and `PUT`(configurable) verb
*Exposing `replaceById` and `replaceOrCreate` via
`POST` and `PUT`(configurable) verb
2016-08-15 12:17:36 -04:00
Loay 25a86906b3 Merge pull request #2618 from strongloop/backport/bcrypt-validation
Add bcrypt validation [2.x]
2016-08-15 11:16:15 -04:00
Loay d8aa6bdf00 Add bcrypt validation
https://github.com/strongloop/loopback/pull/2580
2016-08-15 09:55:23 -04:00
Miroslav Bajtoš c83f84cd05 Merge pull request #2613 from strongloop/fix/globalize-perf-2x
Fix performance of globalization
2016-08-15 08:51:44 +02:00
Miroslav Bajtoš 3b88753c8e Cache remoting descriptions to speed up tests
Calling strong-globalize is relatively expensive (about 300
microseconds for each call). When using per-app local registry in
unit-tests, each app creation requires about 500 calls of `g.t` just
for remoting metadata, i.e. about 150ms.

In this commit, we introduce `g.s` that caches the results from
strong-globalize to speed up creation of remoting metadata.
2016-08-12 13:51:12 +02:00
Miroslav Bajtoš 1dab10da3c Revert globalization of assert() messages 2016-08-12 13:51:12 +02:00
Miroslav Bajtoš 3719ac4bb9 Merge pull request #2604 from strongloop/fix/lazy-cls
Fix token middleware to not trigger CLS init
2016-08-12 09:32:03 +02:00
Miroslav Bajtoš 5978cb4919 Fix token middleware to not trigger CLS init
Rework the token middleware to access current context via
`req.loopbackContext` instead of `loopback.getCurentContext()`.
That way the CLS/AsyncListener machinery is configured only in
applications that are using current context.
2016-08-11 18:19:31 +02:00
Miroslav Bajtoš 4c8ad2908b Merge pull request #2605 from strongloop/feature/key-value-model-2x
common: add KeyValueModel
2016-08-10 16:10:29 +02:00
Miroslav Bajtoš 99dc1f9541 common: add KeyValueModel 2016-08-10 15:30:15 +02:00
Miroslav Bajtoš bb1af5a691 Merge pull request #2559 from strongloop/deprecate/getCurrentContext
Deprecate current-context API
2016-08-10 13:29:51 +02:00
Miroslav Bajtoš b08a1cfba3 Globalize current-context deprecation messages 2016-08-10 12:40:38 +02:00
Miroslav Bajtoš ca28e7ff9e Deprecate current-context API
Deprecate all current-context APIs in favour of loopback-context-cls.
2016-08-10 10:58:33 +02:00
Miroslav Bajtoš 81318e603d test: increase timeout to prevent CI failures
[back-port of #2591]
2016-08-08 16:09:08 +02:00
Candy 16c78a83a1 Merge pull request #2581 from strongloop/backport/globalization
Backport of #2407
2016-08-05 14:44:36 -04:00
Candy 3767940472 Backport of #2407 2016-08-05 11:55:32 -04:00
Miroslav Bajtoš 52f1645713 Merge pull request #2585 from strongloop/update-deps-2x
Update dependencies to their latest versions and fix CI failures [2.x]
2016-08-05 13:35:36 +02:00
Miroslav Bajtoš 0eff26199c test: fix timeout in rest.middleware.test 2016-08-05 11:33:57 +02:00
Miroslav Bajtoš 2eec008e0e test: fix "socket hang up" error in app.test
Rework the test to always wait for the client request to finish before
calling the test done.
2016-08-05 10:54:42 +02:00
Miroslav Bajtoš 593fd6e042 test: increate timeout in Role test 2016-08-05 10:54:42 +02:00
Miroslav Bajtoš fc5f16d833 test: make status test more robust
Rework assertions to report helpful messages on failure.

Increase the "elapsed" limit from 100ms to 300ms to support our
slow CI machines.
2016-08-05 10:54:42 +02:00
Miroslav Bajtoš ed953a4c6f test: fix broken Role tests
Rework the test suite to always report errors and correctly signal
when async tests are done.

This should prevent spurious test failures on CI servers that are
difficult to troubleshoot, because the error is reported for different
test case.
2016-08-05 10:54:42 +02:00
Miroslav Bajtoš fea3b781a0 Update dependencies to their latest versions 2016-08-05 10:54:42 +02:00
jannyHou 7f5f8d6df5 Increase timeout 2016-08-03 11:11:09 -04:00
Candy 4a93935825 Merge pull request #2566 from strongloop/backport/fix_misconfigured-change-replication
Backport of #2565
2016-07-29 15:14:54 -04:00
Amir-61 6fd87c45e7 Merge pull request #2560 from strongloop/avoid-using-deprecated-methods
Avoid calling deprecated methds
2016-07-29 11:44:43 -04:00
Miroslav Bajtoš fa8ac8d324 Backport of #2565 2016-07-29 11:39:47 -04:00
Amir Jafarian 2ab599fdd1 Avoid calling deprecated methds
*Avoid calling deprecated
`getHttpMethod` and `getFullPath`
2016-07-28 11:29:25 -04:00
Miroslav Bajtoš b3c66f34c8 Merge pull request #2555 from strongloop/fix/global-registry-in-fixtures-2x
test: use local registry in test fixtures
2016-07-27 15:29:23 +02:00
Miroslav Bajtoš 895629632f test: use local registry in test fixtures
Use local registry in test fixtures to prevent collision in globally
shared models.

Fix issues discoverd in auth implementation where the global registry
was used instead of the correct local one.
2016-07-27 15:06:32 +02:00
Loay 13cf1aa160 Merge pull request #2553 from strongloop/backport/issue580-test
Fix test case error
2016-07-26 15:40:37 -04:00
Loay a8f30af49d Fix test case error 2016-07-26 13:29:11 -04:00
Loay 5024b50063 Merge pull request #2540 from strongloop/backport/issue580
Backport/issue580
2016-07-25 14:46:58 -04:00
Loay 619372e51e Backport/Fix security issue 580 2016-07-25 11:06:19 -04:00
Miroslav Bajtoš e7bf538a2c 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-07-12 16:53:57 +02:00
Samuel Reed 8bed218a74
Support 'alias' in mail transport config.
Useful if you need to set up multiple transports of the same type.
2016-07-05 10:48:24 -05:00
Jue Hou 8fe77b2a06 Fix description for User.prototype.hasPassword 2016-06-17 17:35:28 -04:00
Loay 4480cd92ab Fix verificationToken bug #2440 2016-06-17 11:16:06 -04:00
Benjamin Kroeger 7e051a7549 add missing unit tests for #2108
Subsequent token middleware tries to read `token.id`
when `enableDoublecheck: true`.
That caused a "Cannot read property `id` of `null`" error
when the first middleware didn't actually find a valid accessToken.

[back-port of #2227]
2016-06-13 15:30:28 +02:00
Miroslav Bajtoš a1c98a8589 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-06-07 17:07:07 +02:00
Miroslav Bajtoš f54a83d530 Merge pull request #2395 from strongloop/deprecate-express-middleware-2.x
[SEMVER-MINOR] Deprecate getters for express 3.x middleware
2016-06-01 10:27:08 +02:00
Miroslav Bajtoš b013e66883 test: increase timeouts on CI 2016-06-01 09:26:45 +02:00
Miroslav Bajtoš 05f8774ed6 jscsrc: remove jsDoc rule
The rule is no longer supported.
2016-05-31 18:58:14 +02:00
Miroslav Bajtoš 75da4c7784 Deprecate getters for express 3.x middleware
In LoopBack 3.0, we are removing these getters, see #2394.
2016-05-31 18:58:10 +02:00
Ritchie Martori 25fe4970e6 Remove env.json and strong-pm dir
[back-port of https://github.com/strongloop/loopback/pull/2327]
2016-05-13 13:46:48 +02:00
Jason 6097fbc005 Fix JSCS unsupported rule error
Replace 'validateJSDoc' rule with 'jsDoc'. 'validateJSDoc' was
deprecated in v1.7.0.

In related news, JSCS was recently deprecated in favor of ESlint
so .jscrc can be removed once features have been rolled over.
2016-05-12 16:56:06 -07:00
Simon Ho 14aed2251c Merge pull request #2317 from strongloop/backport/change-all-hrefs-in-verification-email
Update user.js
2016-05-10 17:15:23 -07:00
juehou 8fef4845f8 Resolver support return promise 2016-05-10 18:00:42 -04:00
Rik 7868803711 Update user.js
allow to change all {href} instances in user.verify() mail into generated url instead of just one
2016-05-09 12:32:05 -07:00
Simon Ho 3f54b07ab8 Merge pull request #2309 from strongloop/backport/separate-error-and-done-logic
Backport separate error checking and done logic
2016-05-06 15:33:36 -07:00
Simon Ho 25ade96d27 Backport separate error checking and done logic 2016-05-06 14:07:38 -07:00
Simon Ho 9311e918cb Merge pull request #2296 from strongloop/backport/do-not-allow-duplicate-role-names
Backport/do not allow duplicate role names
2016-05-05 11:35:42 -07:00
Supasate Choochaisri e89fbd7ce8 Clean up by removing unnecessary comments
Signed-off-by: Supasate Choochaisri <supasate.c@gmail.com>
2016-05-05 10:51:40 -07:00
Supasate Choochaisri 4798b2f8c9 Add feature to not allow duplicate role name
- Also fix jshint error in backported test
2016-05-05 10:51:28 -07:00
Miroslav Bajtoš 692c67384a Merge pull request #2288 from strongloop/support-node-v6-in-2.x
Support node v6 in 2.x
2016-05-04 09:16:46 +02:00
Ryan Graham 4d6f2e7ab7
update/insert copyright notices 2016-05-03 17:10:46 -07:00
Ryan Graham 553889b378
relicense as MIT only 2016-05-03 17:09:47 -07:00
Miroslav Bajtoš e2b1f78f1e Upgrade phantomjs to 2.x 2016-05-03 19:00:01 +02:00
Miroslav Bajtoš da2fb0ae15 app: send port:0 instead of port:undefined
Node v6 no longer supports port:undefined, this commit is fixing
app.listen() to correctly send port:0 when no port is specified.
2016-05-03 19:00:01 +02:00
Miroslav Bajtoš bd7f2b6db1 travis: drop node@5, add node@6 2016-05-03 19:00:01 +02:00
Miroslav Bajtoš c2ad201bf6 Merge pull request #2282 from strongloop/feature/remove-auto-attach-2.x
Clean up unit-tests - use local registry [2.x]
2016-05-03 16:10:45 +02:00
Miroslav Bajtoš 6c59390754 Disable DEBUG output for eslint on Jenkins CI 2016-05-03 14:40:19 +02:00
Miroslav Bajtoš 53cd449c9c test/rest.middleware: use local registry
Rework tests in `test/rest.middleware.test.js` to not depend
on `app.autoAttach()` and global shared registry of Models. Instead,
each tests creates a fresh app instance with a new in-memory datasource
and a new set of Models.
2016-05-03 14:40:17 +02:00
Miroslav Bajtoš cae9786f0e Fix role.isOwner to support app-local registry 2016-05-03 14:39:29 +02:00
Miroslav Bajtoš 845c59eced test/user: use local registry
Rework User tests to not depend on `app.autoAttach()` and global shared
registry of Models. Instead, each tests creates a fresh app instance
with a new in-memory datasource and a new set of Models.
2016-05-03 14:39:06 +02:00
Miroslav Bajtoš 6d738690c8 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-05-02 13:00:09 +02:00
Simon Ho 091c71df23 Merge pull request #2271 from strongloop/backport-emit-disable-remote-method-event
BACKPORT Add new feature to emit a `remoteMethodDisabled` event when disabling
2016-04-29 19:48:57 -07:00
Supasate Choochaisri 2498c02f31 Add new feature to emit a `remoteMethodDisabled` event when disabling a remote method.
Signed-off-by: Supasate Choochaisri <supasate.c@gmail.com>
2016-04-29 16:29:50 -07:00
Tim Needham 1ea1cd612a Fix typo in Model.nestRemoting
Prevent apps from crashing when using `Model.nestRemoting` without
`{ hooks: false }` option.

Note that it is not possible to reproduce this bug using our current
Mocha test suite, because other tests modify the global state in such
way that the bug no longer occurs.

[back-port of #2245]
2016-04-20 09:35:10 +02:00
Miroslav Bajtoš 4678cb4b7c Merge pull request #2204 from strongloop/backport/pr2108
Allow built-in token middleware to run repeatedly [back-port to 2.x]
2016-04-06 16:06:38 +02:00
Benjamin Kröger e4b275243f Allow built-in token middleware to run repeatedly
Add two new options:

  - When `enableDoublecheck` is true, the middleware will run
    even if a previous middleware has already set `req.accessToken`
    (possibly to `null` for anonymous requests)

  - When `overwriteExistingToken` is true (and `enableDoublecheck` too),
    the middleware will overwrite `req.accessToken` set by a previous
    middleware instances.
2016-04-06 15:47:32 +02:00
Miroslav Bajtoš 50e3578992 Improve error message on connector init error
[back-port of pull request #2105]
2016-03-02 13:19:20 +01:00
Sam Roberts a4c643e890 application: correct spelling of "cannont"
[back-port of pull request #2088]
2016-02-19 13:12:51 +01:00
Miroslav Bajtoš 97f376c239 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)
2016-02-19 10:29:24 +01:00
Candy 70aec01e84 Remove sl-blip from dependency 2016-02-18 18:02:00 +01:00
Miroslav Bajtoš e98ed99fe7 Fix race condition in replication tests 2016-02-05 12:39:20 +01:00
Ryan Graham a0806eab89 test: remove errant console.log from test
Using console.log like this can result in invalid xml when the xunit
reporter is used.

[Backport of pull request #2035]
2016-02-05 09:22:43 +01:00
Jue Hou 7a54da5870 Promisify Model Change
* Change.diff
* Change.findOrCreateChange
* Change.rectifyModelChanges
* Change.prototype.currentRevision
* Change.prototype.rectify
2016-02-04 16:01:45 -05:00
Miroslav Bajtoš 7d3d4f1cee Merge pull request #2034 from strongloop/fix/ci
Fix race condition in error handler test
2016-02-04 17:48:52 +01:00
Miroslav Bajtoš 76ec49c96b Fix race condition in error handler test 2016-02-04 16:59:28 +01:00
Miroslav Bajtoš d123e95014 Merge pull request #2033 from strongloop/fix/travis-platforms-2.x
Fix travis platforms [2.x]
2016-02-04 16:44:33 +01:00
Miroslav Bajtoš 4753373f4f Travis: drop iojs, add v4.x and v5.x 2016-02-04 16:28:01 +01:00
Miroslav Bajtoš 015e9cb80e Correct JSDoc findOrCreate() callback in PersistedModel
Update PersistedModel.findOrCreate() JSDoc to reflect the
callback accepts an additional created boolean parameter.

This is a back-port of pull request #1983 from noderat/patch-1
2016-02-04 16:27:08 +01:00
Miroslav Bajtoš a0a1083564 Hide verificationToken
We should never be showing this publically.

Adds unit test for hiding verification token.

This is a back-port of pull request #1851 from gausie/patch-4
2016-02-04 16:27:03 +01:00
Miroslav Bajtoš 870e1010a8 Merge pull request #2032 from strongloop/safer-tests-step1-2.x
Safer tests - step1 [back-port to 2.x]
2016-02-04 16:25:39 +01:00
Ryan Graham db0678baa6 test: use ephemeral port for e2e server 2016-02-04 16:12:27 +01:00
Ryan Graham aff49ff63f test: fail on error instead of crash
If the supertest request fails its basic assertions, there may not even
be a body to perform checks against, so bail early when possible.
2016-02-04 16:12:27 +01:00
Ryan Graham 17bd101691 ensure app is booted before integration tests 2016-02-04 16:12:27 +01:00
Amir Jafarian 8deec2e89a Checkpoint speedup 2016-01-19 10:28:53 -05:00
crandmck 5974c6afdf Pull in API doc fix from PR into master #1910 2016-01-08 17:10:44 -08:00
157 changed files with 8491 additions and 1512 deletions

39
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,39 @@
<!--
- DO NOT ask questions using GitHub issues (only bug or feature requests please,
see http://loopback.io/doc/en/contrib/Reporting-issues.html#asking-questions)
- Please ask questions at https://groups.google.com/forum/#!forum/loopbackjs or
https://gitter.im/strongloop/loopback
- Immediate support is available through our subscription plans, see
https://strongloop.com/node-js/subscription-plans
-->
### Bug or feature request
<!--
Mark your choice with an "x" (eg. [x], NOT [*]).
-->
- [ ] 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)

19
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
### Description
#### Related issues
<!--
Please use the following link syntaxes:
- #49 (to reference issues in the current repository)
- strongloop/loopback#49 (to reference issues in another repository)
-->
- 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)

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
node_modules node_modules
dist dist
*xunit.xml *xunit.xml
.nyc_output/

View File

@ -10,14 +10,11 @@
], ],
"disallowMultipleVarDecl": "exceptUndefined", "disallowMultipleVarDecl": "exceptUndefined",
"disallowSpacesInsideObjectBrackets": null, "disallowSpacesInsideObjectBrackets": null,
"jsDoc": false,
"requireDotNotation": false,
"maximumLineLength": { "maximumLineLength": {
"value": 150, "value": 150,
"allowComments": true, "allowComments": true,
"allowRegex": true "allowRegex": true
},
"validateJSDoc": {
"checkParamNames": false,
"checkRedundantParams": false,
"requireParamTypes": true
} }
} }

7
.nycrc Normal file
View File

@ -0,0 +1,7 @@
{
"exclude": [
"Gruntfile.js",
"test/**/*.js"
],
"cache": true
}

View File

View File

@ -1,7 +1,21 @@
sudo: false sudo: false
language: node_js language: node_js
node_js: node_js:
- "0.10" - "4"
- "0.12" - "6"
- "iojs" - "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"

View File

@ -1,3 +1,414 @@
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 2015-12-22, Version 2.26.2
========================== ==========================
@ -989,8 +1400,6 @@
2014-07-15, Version 2.0.0-beta6 2014-07-15, Version 2.0.0-beta6
=============================== ===============================
* 2.0.0-beta6 (Miroslav Bajtoš)
* lib/application: publish Change models to REST API (Miroslav Bajtoš) * lib/application: publish Change models to REST API (Miroslav Bajtoš)
* models/change: fix typo (Miroslav Bajtoš) * models/change: fix typo (Miroslav Bajtoš)
@ -1001,8 +1410,6 @@
2014-07-03, Version 2.0.0-beta5 2014-07-03, Version 2.0.0-beta5
=============================== ===============================
* 2.0.0-beta5 (Miroslav Bajtoš)
* app: update `url` on `listening` event (Miroslav Bajtoš) * app: update `url` on `listening` event (Miroslav Bajtoš)
* Fix "ReferenceError: loopback is not defined" in registry.memory(). (Guilherme Cirne) * Fix "ReferenceError: loopback is not defined" in registry.memory(). (Guilherme Cirne)
@ -1021,8 +1428,6 @@
2014-06-26, Version 2.0.0-beta4 2014-06-26, Version 2.0.0-beta4
=============================== ===============================
* 2.0.0-beta4 (Miroslav Bajtoš)
* package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš)
* Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš)
@ -1149,8 +1554,6 @@
2014-05-28, Version 2.0.0-beta3 2014-05-28, Version 2.0.0-beta3
=============================== ===============================
* 2.0.0-beta3 (Miroslav Bajtoš)
* package.json: fix malformed json (Miroslav Bajtoš) * package.json: fix malformed json (Miroslav Bajtoš)
* 2.0.0-beta2 (Ritchie Martori) * 2.0.0-beta2 (Ritchie Martori)

View File

@ -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*/ /*global module:false*/
module.exports = function(grunt) { module.exports = function(grunt) {
@ -39,9 +44,6 @@ module.exports = function(grunt) {
common: { common: {
src: ['common/**/*.js'] src: ['common/**/*.js']
}, },
browser: {
src: ['browser/**/*.js']
},
server: { server: {
src: ['server/**/*.js'] src: ['server/**/*.js']
}, },
@ -54,7 +56,6 @@ module.exports = function(grunt) {
lib: ['lib/**/*.js'], lib: ['lib/**/*.js'],
common: ['common/**/*.js'], common: ['common/**/*.js'],
server: ['server/**/*.js'], server: ['server/**/*.js'],
browser: ['browser/**/*.js'],
test: ['test/**/*.js'] test: ['test/**/*.js']
}, },
watch: { watch: {
@ -87,7 +88,8 @@ module.exports = function(grunt) {
src: 'test/*.js', src: 'test/*.js',
options: { options: {
reporter: 'dot', reporter: 'dot',
} require: require.resolve('./test/helpers/use-english.js'),
},
}, },
'unit-xml': { 'unit-xml': {
src: 'test/*.js', src: 'test/*.js',
@ -217,7 +219,14 @@ module.exports = function(grunt) {
grunt.registerTask('e2e-server', function() { grunt.registerTask('e2e-server', function() {
var done = this.async(); var done = this.async();
var app = require('./test/fixtures/e2e/app'); 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']); grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']);
@ -229,7 +238,9 @@ module.exports = function(grunt) {
'jscs', 'jscs',
'jshint', 'jshint',
process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit', 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` // alias for sl-ci-run and `npm test`
grunt.registerTask('mocha-and-karma', ['test']); grunt.registerTask('mocha-and-karma', ['test']);

25
LICENSE Normal file
View File

@ -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.

View File

@ -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

View File

@ -73,6 +73,20 @@ StrongLoop provides a number of example applications that illustrate various key
See [loopback-example](https://github.com/strongloop/loopback-example) for details. See [loopback-example](https://github.com/strongloop/loopback-example) for details.
## Module Long Term Support Policy
LoopBack 2.x is now in maintenance LTS.
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 | Active LTS | Dec 2016 | Dec 2019 |
| Loopback 2 | Maintenance LTS | Jul 2014 | Apr 2019 |
Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html).
## Resources ## Resources
* [Documentation](http://docs.strongloop.com/display/LB/LoopBack). * [Documentation](http://docs.strongloop.com/display/LB/LoopBack).

View File

@ -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.');
};
};

View File

@ -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. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var assert = require('assert'); var assert = require('assert');
var uid = require('uid2'); var uid = require('uid2');
@ -96,7 +103,7 @@ module.exports = function(AccessToken) {
var id = tokenIdForRequest(req, options); var id = tokenIdForRequest(req, options);
if (id) { if (id != null) {
this.findById(id, function(err, token) { this.findById(id, function(err, token) {
if (err) { if (err) {
cb(err); cb(err);
@ -107,7 +114,7 @@ module.exports = function(AccessToken) {
} else if (isValid) { } else if (isValid) {
cb(null, token); cb(null, token);
} else { } else {
var e = new Error('Invalid Access Token'); var e = new Error(g.f('Invalid Access Token'));
e.status = e.statusCode = 401; e.status = e.statusCode = 401;
e.code = 'INVALID_TOKEN'; e.code = 'INVALID_TOKEN';
cb(e); cb(e);
@ -142,11 +149,19 @@ module.exports = function(AccessToken) {
assert(this.ttl, 'token.ttl must exist'); assert(this.ttl, 'token.ttl must exist');
assert(this.ttl >= -1, 'token.ttl must be >= -1'); 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 now = Date.now();
var created = this.created.getTime(); var created = this.created.getTime();
var elapsedSeconds = (now - created) / 1000; var elapsedSeconds = (now - created) / 1000;
var secondsToLive = this.ttl; 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) { if (isValid) {
cb(null, isValid); cb(null, isValid);
@ -194,6 +209,11 @@ module.exports = function(AccessToken) {
if (typeof id === 'string') { if (typeof id === 'string') {
// Add support for oAuth 2.0 bearer token // Add support for oAuth 2.0 bearer token
// http://tools.ietf.org/html/rfc6750 // 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) { if (id.indexOf('Bearer ') === 0) {
id = id.substring(7); id = id.substring(7);
// Decode from base64 // Decode from base64

View File

@ -27,12 +27,6 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "DENY" "permission": "DENY"
},
{
"principalType": "ROLE",
"principalId": "$everyone",
"property": "create",
"permission": "ALLOW"
} }
] ]
} }

View File

@ -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 Schema ACL options
@ -31,6 +36,8 @@
*/ */
var g = require('strong-globalize')();
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
@ -263,7 +270,7 @@ module.exports = function(ACL) {
* @return {Object[]} An array of ACLs * @return {Object[]} An array of ACLs
*/ */
ACL.getStaticACLs = function getStaticACLs(model, property) { ACL.getStaticACLs = function getStaticACLs(model, property) {
var modelClass = loopback.findModel(model); var modelClass = this.registry.findModel(model);
var staticACLs = []; var staticACLs = [];
if (modelClass && modelClass.settings.acls) { if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function(acl) { modelClass.settings.acls.forEach(function(acl) {
@ -355,7 +362,7 @@ module.exports = function(ACL) {
acls = acls.concat(dynACLs); acls = acls.concat(dynACLs);
resolved = self.resolvePermission(acls, req); resolved = self.resolvePermission(acls, req);
if (resolved && resolved.permission === ACL.DEFAULT) { 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; resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
} }
if (callback) callback(null, resolved); if (callback) callback(null, resolved);
@ -387,7 +394,9 @@ module.exports = function(ACL) {
*/ */
ACL.checkAccessForContext = function(context, callback) { ACL.checkAccessForContext = function(context, callback) {
var registry = this.registry; var self = this;
self.resolveRelatedModels();
var roleModel = self.roleModel;
if (!(context instanceof AccessContext)) { if (!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
@ -410,11 +419,9 @@ module.exports = function(ACL) {
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames); var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
var effectiveACLs = []; var effectiveACLs = [];
var staticACLs = this.getStaticACLs(model.modelName, property); var staticACLs = self.getStaticACLs(model.modelName, property);
var self = this; self.find({where: {model: model.modelName, property: propertyQuery,
var roleModel = registry.getModelByType(Role);
this.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function(err, acls) { accessType: accessTypeQuery}}, function(err, acls) {
if (err) { if (err) {
if (callback) callback(err); if (callback) callback(err);
@ -500,10 +507,10 @@ module.exports = function(ACL) {
ACL.resolveRelatedModels = function() { ACL.resolveRelatedModels = function() {
if (!this.roleModel) { if (!this.roleModel) {
var reg = this.registry; var reg = this.registry;
this.roleModel = reg.getModelByType(loopback.Role); this.roleModel = reg.getModelByType('Role');
this.roleMappingModel = reg.getModelByType(loopback.RoleMapping); this.roleMappingModel = reg.getModelByType('RoleMapping');
this.userModel = reg.getModelByType(loopback.User); this.userModel = reg.getModelByType('User');
this.applicationModel = reg.getModelByType(loopback.Application); this.applicationModel = reg.getModelByType('Application');
} }
}; };
@ -530,7 +537,7 @@ module.exports = function(ACL) {
break; break;
default: default:
process.nextTick(function() { 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; err.statusCode = 400;
cb(err); cb(err);
}); });

View File

@ -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 assert = require('assert');
var utils = require('../../lib/utils'); var utils = require('../../lib/utils');

View File

@ -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. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var PersistedModel = require('../../lib/loopback').PersistedModel; var PersistedModel = require('../../lib/loopback').PersistedModel;
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils');
var crypto = require('crypto'); var crypto = require('crypto');
var CJSON = {stringify: require('canonical-json')}; var CJSON = {stringify: require('canonical-json')};
var async = require('async'); var async = require('async');
@ -77,6 +85,8 @@ module.exports = function(Change) {
var Change = this; var Change = this;
var errors = []; var errors = [];
callback = callback || utils.createPromiseCallback();
var tasks = modelIds.map(function(id) { var tasks = modelIds.map(function(id) {
return function(cb) { return function(cb) {
Change.findOrCreateChange(modelName, id, function(err, change) { Change.findOrCreateChange(modelName, id, function(err, change) {
@ -104,13 +114,14 @@ module.exports = function(Change) {
}) })
.join('\n'); .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 = new Error(msg);
err.details = { errors: errors }; err.details = { errors: errors };
return callback(err); return callback(err);
} }
callback(); callback();
}); });
return callback.promise;
}; };
/** /**
@ -137,7 +148,8 @@ module.exports = function(Change) {
*/ */
Change.findOrCreateChange = function(modelName, modelId, callback) { 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 id = this.idForModel(modelName, modelId);
var Change = this; var Change = this;
@ -155,6 +167,7 @@ module.exports = function(Change) {
Change.updateOrCreate(ch, callback); Change.updateOrCreate(ch, callback);
} }
}); });
return callback.promise;
}; };
/** /**
@ -171,9 +184,7 @@ module.exports = function(Change) {
change.debug('rectify change'); change.debug('rectify change');
cb = cb || function(err) { cb = cb || utils.createPromiseCallback();
if (err) throw new Error(err);
};
change.currentRevision(function(err, rev) { change.currentRevision(function(err, rev) {
if (err) return cb(err); if (err) return cb(err);
@ -194,6 +205,7 @@ module.exports = function(Change) {
} }
); );
}); });
return cb.promise;
function doRectify(checkpoint, rev) { function doRectify(checkpoint, rev) {
if (rev) { if (rev) {
@ -248,6 +260,7 @@ module.exports = function(Change) {
*/ */
Change.prototype.currentRevision = function(cb) { Change.prototype.currentRevision = function(cb) {
cb = cb || utils.createPromiseCallback();
var model = this.getModelCtor(); var model = this.getModelCtor();
var id = this.getModelId(); var id = this.getModelId();
model.findById(id, function(err, inst) { model.findById(id, function(err, inst) {
@ -258,6 +271,7 @@ module.exports = function(Change) {
cb(null, null); cb(null, null);
} }
}); });
return cb.promise;
}; };
/** /**
@ -390,8 +404,11 @@ module.exports = function(Change) {
*/ */
Change.diff = function(modelName, since, remoteChanges, callback) { Change.diff = function(modelName, since, remoteChanges, callback) {
callback = callback || utils.createPromiseCallback();
if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) { if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) {
return callback(null, {deltas: [], conflicts: []}); callback(null, {deltas: [], conflicts: []});
return callback.promise;
} }
var remoteChangeIndex = {}; var remoteChangeIndex = {};
var modelIds = []; var modelIds = [];
@ -455,6 +472,7 @@ module.exports = function(Change) {
conflicts: conflicts conflicts: conflicts
}); });
}); });
return callback.promise;
}; };
/** /**

View File

@ -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. * Module Dependencies.
*/ */
@ -27,43 +32,45 @@ module.exports = function(Checkpoint) {
* Get the current checkpoint id * Get the current checkpoint id
* @callback {Function} callback * @callback {Function} callback
* @param {Error} err * @param {Error} err
* @param {Number} checkpointId The current checkpoint id * @param {Number} checkpoint The current checkpoint seq
*/ */
Checkpoint.current = function(cb) { Checkpoint.current = function(cb) {
var Checkpoint = this; var Checkpoint = this;
this.find({ Checkpoint._getSingleton(function(err, cp) {
limit: 1, cb(err, cp.seq);
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.observe('before save', function(ctx, next) { Checkpoint._getSingleton = function(cb) {
if (!ctx.instance) { var query = {limit: 1}; // match all instances, return only one
// Example: Checkpoint.updateAll() and Checkpoint.updateOrCreate() var initialData = {seq: 1};
return next(new Error('Checkpoint does not support partial updates.')); this.findOrCreate(query, initialData, cb);
} };
var model = ctx.instance; /**
if (!model.getId() && model.seq === undefined) { * Increase the current checkpoint if it already exists otherwise initialize it
model.constructor.current(function(err, seq) { * @callback {Function} callback
if (err) return next(err); * @param {Error} err
model.seq = seq + 1; * @param {Object} checkpoint The current checkpoint
next(); */
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(); };
}
});
}; };

View File

@ -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). * Email model. Extends LoopBack base [Model](#model-new-model).
* @property {String} to Email addressee. Required. * @property {String} to Email addressee. Required.
@ -10,6 +15,8 @@
* @inherits {Model} * @inherits {Model}
*/ */
var g = require('strong-globalize')();
module.exports = function(Email) { module.exports = function(Email) {
/** /**
@ -39,13 +46,13 @@ module.exports = function(Email) {
*/ */
Email.send = function() { 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). * A shortcut for Email.send(this).
*/ */
Email.prototype.send = function() { 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'));
}; };
}; };

View File

@ -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);
}

View File

@ -0,0 +1,4 @@
{
"name": "KeyValueModel",
"base": "Model"
}

View File

@ -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 loopback = require('../../lib/loopback');
/** /**

View File

@ -11,7 +11,10 @@
"type": "string", "type": "string",
"description": "The principal type, such as user, application, or role" "description": "The principal type, such as user, application, or role"
}, },
"principalId": "string" "principalId": {
"type": "string",
"index": true
}
}, },
"relations": { "relations": {
"role": { "role": {

View File

@ -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 loopback = require('../../lib/loopback');
var debug = require('debug')('loopback:security:role'); var debug = require('debug')('loopback:security:role');
var assert = require('assert'); var assert = require('assert');
@ -31,9 +36,9 @@ module.exports = function(Role) {
Role.resolveRelatedModels = function() { Role.resolveRelatedModels = function() {
if (!this.userModel) { if (!this.userModel) {
var reg = this.registry; var reg = this.registry;
this.roleMappingModel = reg.getModelByType(loopback.RoleMapping); this.roleMappingModel = reg.getModelByType('RoleMapping');
this.userModel = reg.getModelByType(loopback.User); this.userModel = reg.getModelByType('User');
this.applicationModel = reg.getModelByType(loopback.Application); this.applicationModel = reg.getModelByType('Application');
} }
}; };
@ -75,26 +80,27 @@ module.exports = function(Role) {
}; };
var model = relsToModels[rel]; var model = relsToModels[rel];
listByPrincipalType(model, relsToTypes[rel], query, callback); listByPrincipalType(this, model, relsToTypes[rel], query, callback);
}; };
}); });
/** /**
* Fetch all models assigned to this role * Fetch all models assigned to this role
* @private * @private
* @param {object} Context role context
* @param {*} model model type to fetch * @param {*} model model type to fetch
* @param {String} [principalType] principalType used in the rolemapping for model * @param {String} [principalType] principalType used in the rolemapping for model
* @param {object} [query] query object passed to model find call * @param {object} [query] query object passed to model find call
* @param {Function} [callback] callback function called with `(err, models)` arguments. * @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) { if (callback === undefined) {
callback = query; callback = query;
query = {}; query = {};
} }
roleModel.roleMappingModel.find({ roleModel.roleMappingModel.find({
where: {roleId: this.id, principalType: principalType} where: {roleId: context.id, principalType: principalType},
}, function(err, mappings) { }, function(err, mappings) {
var ids; var ids;
if (err) { if (err) {
@ -123,8 +129,9 @@ module.exports = function(Role) {
/** /**
* Add custom handler for roles. * Add custom handler for roles.
* @param {String} role Name of role. * @param {String} role Name of role.
* @param {Function} resolver Function that determines if a principal is in the specified role. * @param {Function} resolver Function that determines
* Signature must be `function(role, context, callback)` * if a principal is in the specified role.
* Should provide a callback or return a promise.
*/ */
Role.registerResolver = function(role, resolver) { Role.registerResolver = function(role, resolver) {
if (!Role.resolvers) { if (!Role.resolvers) {
@ -147,12 +154,10 @@ module.exports = function(Role) {
}); });
function isUserClass(modelClass) { function isUserClass(modelClass) {
if (modelClass) { if (!modelClass) return false;
return modelClass === loopback.User || var User = modelClass.modelBuilder.models.User;
modelClass.prototype instanceof loopback.User; if (!User) return false;
} else { return modelClass == User || modelClass.prototype instanceof User;
return false;
}
} }
/*! /*!
@ -292,7 +297,14 @@ module.exports = function(Role) {
var resolver = Role.resolvers[role]; var resolver = Role.resolvers[role];
if (resolver) { if (resolver) {
debug('Custom resolver found for role %s', role); 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; return;
} }
@ -371,7 +383,12 @@ module.exports = function(Role) {
* @param {Error} err Error object. * @param {Error} err Error object.
* @param {String[]} roles An array of role IDs * @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)) { if (!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
} }
@ -421,15 +438,24 @@ module.exports = function(Role) {
if (principalType && principalId) { if (principalType && principalId) {
// Please find() treat undefined matches all values // Please find() treat undefined matches all values
inRoleTasks.push(function(done) { inRoleTasks.push(function(done) {
roleMappingModel.find({where: {principalType: principalType, var filter = {where: {principalType: principalType, principalId: principalId}};
principalId: principalId}}, function(err, mappings) { if (options.returnOnlyRoleNames === true) {
filter.include = ['role'];
}
roleMappingModel.find(filter, function(err, mappings) {
debug('Role mappings found: %s %j', err, mappings); debug('Role mappings found: %s %j', err, mappings);
if (err) { if (err) {
if (done) done(err); if (done) done(err);
return; return;
} }
mappings.forEach(function(m) { 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(); if (done) done();
}); });
@ -442,4 +468,6 @@ module.exports = function(Role) {
if (callback) callback(err, roles); if (callback) callback(err, roles);
}); });
}; };
Role.validatesUniquenessOf('name', { message: 'already exists' });
}; };

View File

@ -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 assert = require('assert');
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');

View File

@ -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. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var isEmail = require('isemail');
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils'); var utils = require('../../lib/utils');
var path = require('path'); var path = require('path');
var qs = require('querystring');
var SALT_WORK_FACTOR = 10; var SALT_WORK_FACTOR = 10;
var crypto = require('crypto'); var crypto = require('crypto');
var MAX_PASSWORD_LENGTH = 72;
var bcrypt; var bcrypt;
try { try {
// Try the native module first // Try the native module first
@ -200,14 +209,14 @@ module.exports = function(User) {
realmDelimiter); realmDelimiter);
if (realmRequired && !query.realm) { if (realmRequired && !query.realm) {
var err1 = new Error('realm is required'); var err1 = new Error(g.f('{{realm}} is required'));
err1.statusCode = 400; err1.statusCode = 400;
err1.code = 'REALM_REQUIRED'; err1.code = 'REALM_REQUIRED';
fn(err1); fn(err1);
return fn.promise; return fn.promise;
} }
if (!query.email && !query.username) { 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.statusCode = 400;
err2.code = 'USERNAME_EMAIL_REQUIRED'; err2.code = 'USERNAME_EMAIL_REQUIRED';
fn(err2); fn(err2);
@ -215,7 +224,7 @@ module.exports = function(User) {
} }
self.findOne({where: query}, function(err, user) { 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.statusCode = 401;
defaultError.code = 'LOGIN_FAILED'; defaultError.code = 'LOGIN_FAILED';
@ -245,7 +254,7 @@ module.exports = function(User) {
if (self.settings.emailVerificationRequired && !user.emailVerified) { if (self.settings.emailVerificationRequired && !user.emailVerified) {
// Fail to log in if email verification is not done yet // Fail to log in if email verification is not done yet
debug('User email has not been verified'); 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.statusCode = 401;
err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED'; err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED';
fn(err); fn(err);
@ -285,23 +294,49 @@ module.exports = function(User) {
User.logout = function(tokenId, fn) { User.logout = function(tokenId, fn) {
fn = fn || utils.createPromiseCallback(); 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) { if (err) {
fn(err); fn(err);
} else if (accessToken) { } else if ('count' in info && info.count === 0) {
accessToken.destroy(fn); err = new Error(g.f('Could not find {{accessToken}}'));
err.status = 401;
fn(err);
} else { } else {
fn(new Error('could not find accessToken')); fn();
} }
}); });
return fn.promise; 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. * Compare the given `password` with the users hashed password.
* *
* @param {String} password The plain text 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) { User.prototype.hasPassword = function(plain, fn) {
@ -341,6 +376,10 @@ module.exports = function(User) {
* @property {String} text Text of email. * @property {String} text Text of email.
* @property {String} template Name of template that displays verification * @property {String} template Name of template that displays verification
* page, for example, `'verify.ejs'. * 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 * @property {String} redirect Page to which user will be redirected after
* they verify their email, for example `'/'` for root URI. * they verify their email, for example `'/'` for root URI.
* @property {Function} generateVerificationToken A function to be used to * @property {Function} generateVerificationToken A function to be used to
@ -356,6 +395,7 @@ module.exports = function(User) {
var user = this; var user = this;
var userModel = this.constructor; var userModel = this.constructor;
var registry = userModel.registry; var registry = userModel.registry;
var pkName = userModel.definition.idName() || 'id';
assert(typeof options === 'object', 'options required when calling user.verify()'); assert(typeof options === 'object', 'options required when calling user.verify()');
assert(options.type, 'You must supply a verification type (options.type)'); assert(options.type, 'You must supply a verification type (options.type)');
assert(options.type === 'email', 'Unsupported verification type'); assert(options.type === 'email', 'Unsupported verification type');
@ -377,18 +417,24 @@ module.exports = function(User) {
(options.protocol === 'https' && options.port == '443') (options.protocol === 'https' && options.port == '443')
) ? '' : ':' + options.port; ) ? '' : ':' + options.port;
var urlPath = joinUrlPath(
options.restApiRoot,
userModel.http.path,
userModel.sharedClass.find('confirm', true).http.path
);
options.verifyHref = options.verifyHref || options.verifyHref = options.verifyHref ||
options.protocol + options.protocol +
'://' + '://' +
options.host + options.host +
displayPort + displayPort +
options.restApiRoot + urlPath +
userModel.http.path + '?' + qs.stringify({
userModel.sharedClass.find('confirm', true).http.path + uid: '' + options.user[pkName],
'?uid=' + redirect: options.redirect,
options.user.id + });
'&redirect=' +
options.redirect; options.templateFn = options.templateFn || createVerificationEmailBody;
// Email model // Email model
var Email = options.mailer || this.constructor.email || registry.getModelByType(loopback.Email); var Email = options.mailer || this.constructor.email || registry.getModelByType(loopback.Email);
@ -413,30 +459,50 @@ module.exports = function(User) {
function sendEmail(user) { function sendEmail(user) {
options.verifyHref += '&token=' + user.verificationToken; options.verifyHref += '&token=' + user.verificationToken;
options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; options.text = options.text || 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.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 || {}; options.headers = options.headers || {};
var template = loopback.template(options.template); options.templateFn(options, function(err, html) {
options.html = template(options);
Email.send(options, function(err, email) {
if (err) { if (err) {
fn(err); fn(err);
} else { } 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; 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 * A default verification token generator which accepts the user the token is
* being generated for and a callback function to indicate completion. * being generated for and a callback function to indicate completion.
@ -469,7 +535,7 @@ module.exports = function(User) {
fn(err); fn(err);
} else { } else {
if (user && user.verificationToken === token) { if (user && user.verificationToken === token) {
user.verificationToken = undefined; user.verificationToken = null;
user.emailVerified = true; user.emailVerified = true;
user.save(function(err) { user.save(function(err) {
if (err) { if (err) {
@ -480,11 +546,11 @@ module.exports = function(User) {
}); });
} else { } else {
if (user) { if (user) {
err = new Error('Invalid token: ' + token); err = new Error(g.f('Invalid token: %s', token));
err.statusCode = 400; err.statusCode = 400;
err.code = 'INVALID_TOKEN'; err.code = 'INVALID_TOKEN';
} else { } else {
err = new Error('User not found: ' + uid); err = new Error(g.f('User not found: %s', uid));
err.statusCode = 404; err.statusCode = 404;
err.code = 'USER_NOT_FOUND'; err.code = 'USER_NOT_FOUND';
} }
@ -496,11 +562,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. * to change passwords if forgotten.
* *
* @options {Object} options * @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 * @callback {Function} callback
* @param {Error} err * @param {Error} err
*/ */
@ -509,29 +576,48 @@ module.exports = function(User) {
cb = cb || utils.createPromiseCallback(); cb = cb || utils.createPromiseCallback();
var UserModel = this; var UserModel = this;
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
options = options || {}; options = options || {};
if (typeof options.email !== 'string') { 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.statusCode = 400;
err.code = 'EMAIL_REQUIRED'; err.code = 'EMAIL_REQUIRED';
cb(err); cb(err);
return cb.promise; 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) { if (err) {
return cb(err); return cb(err);
} }
if (!user) { if (!user) {
err = new Error('Email not found'); err = new Error(g.f('Email not found'));
err.statusCode = 404; err.statusCode = 404;
err.code = 'EMAIL_NOT_FOUND'; err.code = 'EMAIL_NOT_FOUND';
return cb(err); return cb(err);
} }
// create a short lived access token for temp login to change password // create a short lived access token for temp login to change password
// TODO(ritch) - eventually this should only allow password change // 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) { if (err) {
return cb(err); return cb(err);
} }
@ -539,7 +625,8 @@ module.exports = function(User) {
UserModel.emit('resetPasswordRequest', { UserModel.emit('resetPasswordRequest', {
email: options.email, email: options.email,
accessToken: accessToken, accessToken: accessToken,
user: user user: user,
options: options,
}); });
}); });
}); });
@ -557,14 +644,45 @@ module.exports = function(User) {
}; };
User.validatePassword = function(plain) { User.validatePassword = function(plain) {
if (typeof plain === 'string' && plain) { var err;
if (plain && typeof plain === 'string' && plain.length <= MAX_PASSWORD_LENGTH) {
return true; 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; err.statusCode = 422;
throw err; 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. * Setup an extended user model.
*/ */
@ -599,14 +717,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 // Make sure emailVerified is not set by creation
UserModel.beforeRemote('create', function(ctx, user, next) { UserModel.beforeRemote('create', function(ctx, user, next) {
var body = ctx.req.body; var body = ctx.req.body;
@ -622,17 +732,18 @@ module.exports = function(User) {
description: 'Login a user with username/email and password.', description: 'Login a user with username/email and password.',
accepts: [ accepts: [
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
{arg: 'include', type: ['string'], http: {source: 'query' }, {arg: 'include', type: ['string'], http: {source: 'query'},
description: 'Related objects to include in the response. ' + 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: { returns: {
arg: 'accessToken', type: 'object', root: true, arg: 'accessToken', type: 'object', root: true,
description: 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 ' + 'Depending on the value of `include` parameter, the body may contain ' +
'additional properties:\n\n' + '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'} http: {verb: 'post'}
} }
@ -643,15 +754,14 @@ module.exports = function(User) {
{ {
description: 'Logout a user with access token.', description: 'Logout a user with access token.',
accepts: [ accepts: [
{arg: 'access_token', type: 'string', required: true, http: function(ctx) { {arg: 'access_token', type: 'string', http: function(ctx) {
var req = ctx && ctx.req; var req = ctx && ctx.req;
var accessToken = req && req.accessToken; var accessToken = req && req.accessToken;
var tokenID = accessToken && accessToken.id; var tokenID = accessToken ? accessToken.id : undefined;
return tokenID;
return tokenID; }, description: 'Do not supply this argument, it is automatically extracted ' +
}, description: 'Do not supply this argument, it is automatically extracted ' + 'from request headers.',
'from request headers.' },
}
], ],
http: {verb: 'all'} http: {verb: 'all'}
} }
@ -684,7 +794,7 @@ module.exports = function(User) {
UserModel.afterRemote('confirm', function(ctx, inst, next) { UserModel.afterRemote('confirm', function(ctx, inst, next) {
if (ctx.args.redirect !== undefined) { if (ctx.args.redirect !== undefined) {
if (!ctx.res) { 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.location(ctx.args.redirect);
ctx.res.status(302); ctx.res.status(302);
@ -699,10 +809,9 @@ module.exports = function(User) {
assert(loopback.AccessToken, 'AccessToken model must be defined before User model'); assert(loopback.AccessToken, 'AccessToken model must be defined before User model');
UserModel.accessToken = loopback.AccessToken; UserModel.accessToken = loopback.AccessToken;
// email validation regex UserModel.validate('email', emailValidator, {
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,}))$/; message: g.f('Must provide a valid email')
});
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
// FIXME: We need to add support for uniqueness of composite keys in juggler // FIXME: We need to add support for uniqueness of composite keys in juggler
if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) {
@ -710,6 +819,34 @@ module.exports = function(User) {
UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); 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; return UserModel;
}; };
@ -719,4 +856,105 @@ module.exports = function(User) {
User.setup(); 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) {
emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email;
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;
}

View File

@ -32,7 +32,7 @@
"options": { "options": {
"caseSensitiveEmail": true "caseSensitiveEmail": true
}, },
"hidden": ["password"], "hidden": ["password", "verificationToken"],
"acls": [ "acls": [
{ {
"principalType": "ROLE", "principalType": "ROLE",
@ -75,6 +75,12 @@
"permission": "ALLOW", "permission": "ALLOW",
"property": "updateAttributes" "property": "updateAttributes"
}, },
{
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "replaceById"
},
{ {
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",

View File

@ -5,7 +5,7 @@
"lib/server-app.js", "lib/server-app.js",
"lib/loopback.js", "lib/loopback.js",
"lib/registry.js", "lib/registry.js",
"server/current-context.js", "lib/current-context.js",
"lib/access-context.js", "lib/access-context.js",
{ "title": "Base models", "depth": 2 }, { "title": "Base models", "depth": 2 },
"lib/model.js", "lib/model.js",
@ -24,6 +24,7 @@
"common/models/application.js", "common/models/application.js",
"common/models/change.js", "common/models/change.js",
"common/models/email.js", "common/models/email.js",
"common/models/key-value-model.js",
"common/models/role.js", "common/models/role.js",
"common/models/role-mapping.js", "common/models/role-mapping.js",
"common/models/scope.js", "common/models/scope.js",

View File

@ -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 loopback = require('../../');
var client = loopback(); var client = loopback();
var CartItem = require('./models').CartItem; var CartItem = require('./models').CartItem;
@ -11,10 +18,10 @@ CartItem.attachTo(remote);
// call the remote method // call the remote method
CartItem.sum(1, function(err, total) { CartItem.sum(1, function(err, total) {
console.log('result:', err || total); g.log('result:%s', err || total);
}); });
// call a built in remote method // call a built in remote method
CartItem.find(function(err, items) { CartItem.find(function(err, items) {
console.log(items); g.log(items);
}); });

View File

@ -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 loopback = require('../../');
var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', { var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', {

View File

@ -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 loopback = require('../../');
var server = module.exports = loopback(); var server = module.exports = loopback();
var CartItem = require('./models').CartItem; var CartItem = require('./models').CartItem;

View File

@ -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 loopback = require('../../');
var app = loopback(); var app = loopback();
@ -7,9 +14,9 @@ var schema = {
name: String name: String
}; };
var Color = app.model('color', schema); app.dataSource('db', { connector: 'memory' });
var Color = app.registry.createModel('color', schema);
app.dataSource('db', {adapter: 'memory'}).attach(Color); app.model(Color, { dataSource: 'db' });
Color.create({name: 'red'}); Color.create({name: 'red'});
Color.create({name: 'green'}); Color.create({name: 'green'});
@ -17,4 +24,4 @@ Color.create({name: 'blue'});
app.listen(3000); 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}}');

View File

@ -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 loopback = require('../../');
var app = loopback(); var app = loopback();
@ -17,7 +24,7 @@ var Color = loopback.createModel('color', { 'name': String });
Color.beforeRemote('**', function (ctx, unused, next) { Color.beforeRemote('**', function (ctx, unused, next) {
// Inside LoopBack code, you can read the property from the context // Inside LoopBack code, you can read the property from the context
var ns = loopback.getCurrentContext(); var ns = loopback.getCurrentContext();
console.log('Request to host', ns && ns.get('host')); g.log('Request to host %s', ns && ns.get('host'));
next(); next();
}); });
@ -25,5 +32,5 @@ app.dataSource('db', { connector: 'memory' });
app.model(Color, { dataSource: 'db' }); app.model(Color, { dataSource: 'db' });
app.listen(3000, function() { 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}}');
}); });

View File

@ -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 models = require('../../lib/models');
var loopback = require('../../'); var loopback = require('../../');
@ -32,14 +39,15 @@ var data = {pushSettings: [
]} ]}
Application.create(data, function(err, data) { 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) { 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()); console.log(result.toObject());
});
result.resetKeys(function (err, result) {
console.log(result.toObject());
});
}); });

View File

@ -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 loopback = require('../../');
var app = loopback(); var app = loopback();
var db = app.dataSource('db', {connector: loopback.Memory}); var db = app.dataSource('db', { connector: 'memory' });
var Color = app.model('color', {dataSource: 'db', options: {trackChanges: true}}); var Color = app.registry.createModel('color', {}, { trackChanges: true });
var Color2 = app.model('color2', {dataSource: 'db', options: {trackChanges: true}}); app.model(Color, { dataSource: 'db' });
var Color2 = app.registry.createModel('color2', {}, { trackChanges: true });
app.model(Color2, { dataSource: 'db' });
var target = Color2; var target = Color2;
var source = Color; var source = Color;
var SPEED = process.env.SPEED || 100; var SPEED = process.env.SPEED || 100;

View File

@ -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 loopback = require('../../');
var app = loopback(); var app = loopback();
@ -20,4 +27,4 @@ Color.all(function () {
app.listen(3000); 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}}');

View File

@ -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 * loopback ~ public api
*/ */

76
intl/de/messages.json Normal file
View File

@ -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}"
}

75
intl/en/messages.json Normal file
View File

@ -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}"
}

76
intl/es/messages.json Normal file
View File

@ -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}"
}

76
intl/fr/messages.json Normal file
View File

@ -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}"
}

76
intl/it/messages.json Normal file
View File

@ -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}"
}

76
intl/ja/messages.json Normal file
View File

@ -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}"
}

76
intl/ko/messages.json Normal file
View File

@ -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}"
}

76
intl/nl/messages.json Normal file
View File

@ -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}"
}

76
intl/pt/messages.json Normal file
View File

@ -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}"
}

76
intl/tr/messages.json Normal file
View File

@ -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}"
}

View File

@ -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}"
}

View File

@ -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}"
}

View File

@ -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 assert = require('assert');
var loopback = require('./loopback'); var loopback = require('./loopback');
var debug = require('debug')('loopback:security:access-context'); var debug = require('debug')('loopback:security:access-context');
@ -56,16 +61,16 @@ function AccessContext(context) {
var principalType = context.principalType || Principal.USER; var principalType = context.principalType || Principal.USER;
var principalId = context.principalId || undefined; var principalId = context.principalId || undefined;
var principalName = context.principalName || undefined; var principalName = context.principalName || undefined;
if (principalId) { if (principalId != null) {
this.addPrincipal(principalType, principalId, principalName); this.addPrincipal(principalType, principalId, principalName);
} }
var token = this.accessToken || {}; var token = this.accessToken || {};
if (token.userId) { if (token.userId != null) {
this.addPrincipal(Principal.USER, token.userId); this.addPrincipal(Principal.USER, token.userId);
} }
if (token.appId) { if (token.appId != null) {
this.addPrincipal(Principal.APPLICATION, token.appId); this.addPrincipal(Principal.APPLICATION, token.appId);
} }
this.remotingContext = context.remotingContext; this.remotingContext = context.remotingContext;
@ -146,7 +151,7 @@ AccessContext.prototype.getAppId = function() {
* @returns {boolean} * @returns {boolean}
*/ */
AccessContext.prototype.isAuthenticated = function() { AccessContext.prototype.isAuthenticated = function() {
return !!(this.getUserId() || this.getAppId()); return this.getUserId() != null || this.getAppId() != null;
}; };
/*! /*!

View File

@ -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. * Module dependencies.
*/ */
var g = require('strong-globalize')();
var DataSource = require('loopback-datasource-juggler').DataSource; var DataSource = require('loopback-datasource-juggler').DataSource;
var Registry = require('./registry'); var Registry = require('./registry');
var assert = require('assert'); var assert = require('assert');
@ -153,6 +160,11 @@ app.model = function(Model, config) {
this.emit('modelRemoted', Model.sharedClass); this.emit('modelRemoted', Model.sharedClass);
} }
var self = this;
Model.on('remoteMethodDisabled', function(model, methodName) {
self.emit('remoteMethodDisabled', model, methodName);
});
Model.shared = isPublic; Model.shared = isPublic;
Model.app = this; Model.app = this;
Model.emit('attached', this); Model.emit('attached', this);
@ -219,11 +231,20 @@ app.models = function() {
* @param {Object} config The data source config * @param {Object} config The data source config
*/ */
app.dataSource = function(name, config) { app.dataSource = function(name, config) {
var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry); try {
this.dataSources[name] = var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
this.dataSources[classify(name)] = this.dataSources[name] =
this.dataSources[camelize(name)] = ds; this.dataSources[classify(name)] =
return ds; 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); var Model = app.registry.findModel(m);
if (!Model) { if (!Model) {
throw new Error( 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) { for (var name in appModels) {
var candidate = appModels[name]; var candidate = appModels[name];
var isSubclass = candidate.prototype instanceof Model; var isSubclass = candidate.prototype instanceof Model;
@ -360,17 +383,17 @@ app.enableAuth = function(options) {
var messages = { var messages = {
403: { 403: {
message: 'Access Denied', message: g.f('Access Denied'),
code: 'ACCESS_DENIED' code: 'ACCESS_DENIED',
}, },
404: { 404: {
message: ('could not find ' + modelName + ' with id ' + modelId), message: (g.f('could not find %s with id %s', modelName, modelId)),
code: 'MODEL_NOT_FOUND' code: 'MODEL_NOT_FOUND',
}, },
401: { 401: {
message: 'Authorization Required', message: g.f('Authorization Required'),
code: 'AUTHORIZATION_REQUIRED' code: 'AUTHORIZATION_REQUIRED',
} },
}; };
var e = new Error(messages[errStatusCode].message || messages[403].message); var e = new Error(messages[errStatusCode].message || messages[403].message);
@ -390,14 +413,14 @@ app.enableAuth = function(options) {
app.boot = function(options) { app.boot = function(options) {
throw new Error( 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) { function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
var connectorPath; var connectorPath;
assert(typeof config === 'object', assert(typeof config === 'object',
'cannont create data source without config object'); 'can not create data source without config object');
if (typeof config.connector === 'string') { if (typeof config.connector === 'string') {
name = config.connector; name = config.connector;
@ -410,6 +433,8 @@ function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
config.connector = require(connectorPath); config.connector = require(connectorPath);
} }
} }
if (!config.connector.name)
config.connector.name = name;
} }
return registry.createDataSource(config); return registry.createDataSource(config);
@ -466,7 +491,7 @@ function setSharedMethodSharedProperties(model, app, modelConfigs) {
var settingValue = settings[setting]; var settingValue = settings[setting];
var settingValueType = typeof settingValue; var settingValueType = typeof settingValue;
if (settingValueType !== 'boolean') 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 // set sharedMethod.shared using the merged settings
@ -551,7 +576,11 @@ app.listen = function(cb) {
(arguments.length == 1 && typeof arguments[0] == 'function'); (arguments.length == 1 && typeof arguments[0] == 'function');
if (useAppConfig) { 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 { } else {
server.listen.apply(server, arguments); server.listen.apply(server, arguments);
} }

View File

@ -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 EventEmitter = require('events').EventEmitter;
var util = require('util'); var util = require('util');

View File

@ -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) { module.exports = function(registry) {
// NOTE(bajtos) we must use static require() due to browserify limitations // 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( registry.Email = createModel(
require('../common/models/email.json'), require('../common/models/email.json'),
require('../common/models/email.js')); require('../common/models/email.js'));

View File

@ -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`. * Expose `Connector`.
*/ */

View File

@ -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. * Dependencies.
*/ */
var g = require('strong-globalize')();
var mailer = require('nodemailer'); var mailer = require('nodemailer');
var assert = require('assert'); var assert = require('assert');
var debug = require('debug')('loopback:connector:mail'); var debug = require('debug')('loopback:connector:mail');
@ -54,10 +61,11 @@ MailConnector.prototype.DataAccessObject = Mailer;
* Example: * Example:
* *
* Email.setupTransport({ * Email.setupTransport({
* type: 'SMTP', * type: "SMTP",
* host: "smtp.gmail.com", // hostname * host: "smtp.gmail.com", // hostname
* secureConnection: true, // use SSL * secureConnection: true, // use SSL
* port: 465, // port for secure SMTP * port: 465, // port for secure SMTP
* alias: "gmail", // optional alias for use with 'transport' option when sending
* auth: { * auth: {
* user: "gmail.user@gmail.com", * user: "gmail.user@gmail.com",
* pass: "userpass" * pass: "userpass"
@ -83,7 +91,7 @@ MailConnector.prototype.setupTransport = function(setting) {
transport = mailer.createTransport(transportModule(setting)); transport = mailer.createTransport(transportModule(setting));
} }
connector.transportsIndex[setting.type] = transport; connector.transportsIndex[setting.alias || setting.type] = transport;
connector.transports.push(transport); connector.transports.push(transport);
}; };
@ -122,7 +130,8 @@ MailConnector.prototype.defaultTransport = function() {
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
* subject: "Hello ✔", // Subject line * subject: "Hello ✔", // Subject line
* text: "Hello world ✔", // plaintext body * text: "Hello world ✔", // plaintext body
* html: "<b>Hello world ✔</b>" // html body * html: "<b>Hello world ✔</b>", // html body
* transport: "gmail", // See 'alias' option above in setupTransport
* } * }
* *
* See https://github.com/andris9/Nodemailer for other supported options. * 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) { if (debug.enabled || settings && settings.debug) {
console.log('Sending Mail:'); g.log('Sending Mail:');
if (options.transport) { if (options.transport) {
console.log('\t TRANSPORT:', options.transport); console.log(g.f('\t TRANSPORT:%s', options.transport));
} }
console.log('\t TO:', options.to); g.log('\t TO:%s', options.to);
console.log('\t FROM:', options.from); g.log('\t FROM:%s', options.from);
console.log('\t SUBJECT:', options.subject); g.log('\t SUBJECT:%s', options.subject);
console.log('\t TEXT:', options.text); g.log('\t TEXT:%s', options.text);
console.log('\t HTML:', options.html); g.log('\t HTML:%s', options.html);
} }
if (transport) { if (transport) {
assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport'); assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport');
transport.sendMail(options, fn); transport.sendMail(options, fn);
} else { } 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.'); ' Setup a transport to send mail messages.');
process.nextTick(function() { process.nextTick(function() {
fn(null, options); fn(null, options);

View File

@ -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`. * Expose `Memory`.
*/ */

78
lib/current-context.js Normal file
View File

@ -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);
};
};

View File

@ -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 path = require('path');
var deprecated = require('depd')('loopback');
var middlewares = exports; var middlewares = exports;
@ -24,12 +30,10 @@ var middlewareModules = {
'cookieParser': 'cookie-parser', 'cookieParser': 'cookie-parser',
'cookieSession': 'cookie-session', 'cookieSession': 'cookie-session',
'csrf': 'csurf', 'csrf': 'csurf',
'errorHandler': 'errorhandler',
'session': 'express-session', 'session': 'express-session',
'methodOverride': 'method-override', 'methodOverride': 'method-override',
'logger': 'morgan', 'logger': 'morgan',
'responseTime': 'response-time', 'responseTime': 'response-time',
'favicon': 'serve-favicon',
'directory': 'serve-index', 'directory': 'serve-index',
// 'static': 'serve-static', // 'static': 'serve-static',
'vhost': 'vhost' 'vhost': 'vhost'
@ -39,14 +43,20 @@ middlewares.bodyParser = safeRequire('body-parser');
middlewares.json = middlewares.bodyParser && middlewares.bodyParser.json; middlewares.json = middlewares.bodyParser && middlewares.bodyParser.json;
middlewares.urlencoded = middlewares.bodyParser && middlewares.bodyParser.urlencoded; 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) { for (var m in middlewareModules) {
var moduleName = middlewareModules[m]; var moduleName = middlewareModules[m];
middlewares[m] = safeRequire(moduleName) || createMiddlewareNotInstalled(m, moduleName); middlewares[m] = safeRequire(moduleName) || createMiddlewareNotInstalled(m, moduleName);
deprecated.property(middlewares, m, deprecationMessage(m, moduleName));
} }
// serve-favicon requires a path function deprecationMessage(accessor, moduleName) {
var favicon = middlewares.favicon; return 'loopback.' + accessor + ' is deprecated. ' +
middlewares.favicon = function(icon, options) { 'Use `require(\'' + moduleName + '\');` instead.';
icon = icon || path.join(__dirname, '../favicon.ico'); }
return favicon(icon, options);
};

View File

@ -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. * 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 * Create a named vanilla JavaScript class constructor with an attached

View File

@ -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. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var assert = require('assert'); var assert = require('assert');
var debug = require('debug')('loopback:model');
var RemoteObjects = require('strong-remoting'); var RemoteObjects = require('strong-remoting');
var SharedClass = require('strong-remoting').SharedClass; var SharedClass = require('strong-remoting').SharedClass;
var extend = require('util')._extend; var extend = require('util')._extend;
var format = require('util').format;
module.exports = function(registry) { module.exports = function(registry) {
@ -116,6 +126,80 @@ module.exports = function(registry) {
var options = this.settings; var options = this.settings;
var typeName = this.modelName; var typeName = this.modelName;
// support remoting prototype methods
// 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;
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;
data = null;
} else {
id = null;
}
}
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 != 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(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(g.f('must specify an {{id}} or {{data}}')));
}
};
var idDesc = ModelCtor.modelName + ' id';
ModelCtor.sharedCtor.accepts = this._removeOptionsArgIfDisabled([
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
description: idDesc},
// {arg: 'instance', type: 'object', http: {source: 'body'}}
{arg: 'options', type: 'object', http: createOptionsViaModelMethod},
]);
ModelCtor.sharedCtor.http = [
{path: '/:id'}
];
ModelCtor.sharedCtor.returns = {root: true};
var remotingOptions = {}; var remotingOptions = {};
extend(remotingOptions, options.remoting || {}); extend(remotingOptions, options.remoting || {});
@ -131,69 +215,13 @@ module.exports = function(registry) {
return val ? new ModelCtor(val) : val; return val ? new ModelCtor(val) : val;
}); });
// support remoting prototype methods
ModelCtor.sharedCtor = function(data, id, fn) {
var ModelCtor = this;
if (typeof data === 'function') {
fn = data;
data = null;
id = null;
} else if (typeof id === 'function') {
fn = id;
if (typeof data !== 'object') {
id = data;
data = null;
} else {
id = null;
}
}
if (id && 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) {
if (err) {
fn(err);
} else if (model) {
fn(null, model);
} else {
err = new Error('could not find a model with id ' + id);
err.statusCode = 404;
err.code = 'MODEL_NOT_FOUND';
fn(err);
}
});
} else {
fn(new Error('must specify an id or data'));
}
};
var idDesc = ModelCtor.modelName + ' id';
ModelCtor.sharedCtor.accepts = [
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
description: idDesc}
// {arg: 'instance', type: 'object', http: {source: 'body'}}
];
ModelCtor.sharedCtor.http = [
{path: '/:id'}
];
ModelCtor.sharedCtor.returns = {root: true};
// before remote hook // before remote hook
ModelCtor.beforeRemote = function(name, fn) { ModelCtor.beforeRemote = function(name, fn) {
var className = this.modelName; var className = this.modelName;
this._runWhenAttachedToApp(function(app) { this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes(); var remotes = app.remotes();
remotes.before(className + '.' + name, function(ctx, next) { 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) { this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes(); var remotes = app.remotes();
remotes.after(className + '.' + name, function(ctx, next) { 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) { sharedClass.resolve(function resolver(define) {
var relations = ModelCtor.relations || {}; 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 // get the relations
for (var relationName in relations) { for (var relationName in relations) {
@ -357,6 +393,8 @@ module.exports = function(registry) {
return ACL.WRITE; return ACL.WRITE;
case 'updateOrCreate': case 'updateOrCreate':
return ACL.WRITE; return ACL.WRITE;
case 'upsertWithWhere':
return ACL.WRITE;
case 'upsert': case 'upsert':
return ACL.WRITE; return ACL.WRITE;
case 'exists': case 'exists':
@ -417,9 +455,40 @@ module.exports = function(registry) {
if (options.isStatic === undefined) { if (options.isStatic === undefined) {
options.isStatic = true; options.isStatic = true;
} }
if (options.accepts) {
options = extend({}, options);
options.accepts = setupOptionsArgs(options.accepts);
}
this.sharedClass.defineMethod(name, options); 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. * Disable remote invocation for the method with the given name.
* *
@ -430,7 +499,21 @@ module.exports = function(registry) {
*/ */
Model.disableRemoteMethod = function(name, isStatic) { 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) { Model.belongsToRemoting = function(relationName, relation, define) {
@ -441,10 +524,13 @@ module.exports = function(registry) {
define('__get__' + relationName, { define('__get__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'get', path: '/' + pathName}, 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', accessType: 'READ',
description: 'Fetches belongsTo relation ' + relationName + '.', description: format('Fetches belongsTo relation %s.', relationName),
returns: {arg: relationName, type: modelName, root: true} returns: {arg: relationName, type: modelName, root: true},
}, fn); }, fn);
}; };
@ -452,7 +538,7 @@ module.exports = function(registry) {
if (ctx.result !== null) return cb(); if (ctx.result !== null) return cb();
var fk = ctx.getArgByName('fk'); 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); var error = new Error(msg);
error.statusCode = error.status = 404; error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND'; error.code = 'MODEL_NOT_FOUND';
@ -466,8 +552,11 @@ module.exports = function(registry) {
define('__get__' + relationName, { define('__get__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'get', path: '/' + pathName}, http: {verb: 'get', path: '/' + pathName},
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, accepts: this._removeOptionsArgIfDisabled([
description: 'Fetches hasOne relation ' + relationName + '.', {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
description: format('Fetches hasOne relation %s.', relationName),
accessType: 'READ', accessType: 'READ',
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)} rest: {after: convertNullToNotFoundError.bind(null, toModelName)}
@ -476,8 +565,14 @@ module.exports = function(registry) {
define('__create__' + relationName, { define('__create__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'post', path: '/' + pathName}, http: {verb: 'post', path: '/' + pathName},
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, accepts: this._removeOptionsArgIfDisabled([
description: 'Creates a new instance in ' + relationName + ' of this model.', {
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', accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true} returns: {arg: 'data', type: toModelName, root: true}
}); });
@ -485,8 +580,14 @@ module.exports = function(registry) {
define('__update__' + relationName, { define('__update__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'put', path: '/' + pathName}, http: {verb: 'put', path: '/' + pathName},
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, accepts: this._removeOptionsArgIfDisabled([
description: 'Update ' + relationName + ' of this model.', {
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', accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true} returns: {arg: 'data', type: toModelName, root: true}
}); });
@ -494,8 +595,11 @@ module.exports = function(registry) {
define('__destroy__' + relationName, { define('__destroy__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'delete', path: '/' + pathName}, http: {verb: 'delete', path: '/' + pathName},
description: 'Deletes ' + relationName + ' of this model.', accepts: this._removeOptionsArgIfDisabled([
accessType: 'WRITE' {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, { define('__findById__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'get', path: '/' + pathName + '/:fk'}, http: {verb: 'get', path: '/' + pathName + '/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: this._removeOptionsArgIfDisabled([
description: 'Foreign key for ' + relationName, required: true, {
http: {source: 'path'}}, arg: 'fk', type: 'any',
description: 'Find a related item by id for ' + relationName + '.', 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', accessType: 'READ',
returns: {arg: 'result', type: toModelName, root: true}, returns: {arg: 'result', type: toModelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)} rest: {after: convertNullToNotFoundError.bind(null, toModelName)}
@ -520,10 +630,16 @@ module.exports = function(registry) {
define('__destroyById__' + relationName, { define('__destroyById__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/:fk'}, http: {verb: 'delete', path: '/' + pathName + '/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: this._removeOptionsArgIfDisabled([
description: 'Foreign key for ' + relationName, required: true, {
http: {source: 'path'}}, arg: 'fk', type: 'any',
description: 'Delete a related item by id for ' + relationName + '.', 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', accessType: 'WRITE',
returns: [] returns: []
}, destroyByIdFunc); }, destroyByIdFunc);
@ -532,13 +648,15 @@ module.exports = function(registry) {
define('__updateById__' + relationName, { define('__updateById__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/:fk'}, http: {verb: 'put', path: '/' + pathName + '/:fk'},
accepts: [ accepts: this._removeOptionsArgIfDisabled([
{arg: 'fk', type: 'any', {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: format('Foreign key for %s', relationName),
http: {source: 'path'}}, required: true,
{arg: 'data', type: toModelName, http: {source: 'body'}} http: { source: 'path' }},
], {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
description: 'Update a related item by id for ' + relationName + '.', {arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
description: format('Update a related item by id for %s.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: {arg: 'result', type: toModelName, root: true} returns: {arg: 'result', type: toModelName, root: true}
}, updateByIdFunc); }, updateByIdFunc);
@ -549,17 +667,21 @@ module.exports = function(registry) {
var accepts = []; var accepts = [];
if (relation.type === 'hasMany' && relation.modelThrough) { if (relation.type === 'hasMany' && relation.modelThrough) {
// Restrict: only hasManyThrough relation can have additional properties // 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]; var addFunc = this.prototype['__link__' + relationName];
define('__link__' + relationName, { define('__link__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/rel/:fk'}, http: {verb: 'put', path: '/' + pathName + '/rel/:fk'},
accepts: [{arg: 'fk', type: 'any', accepts: [{ arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: format('Foreign key for %s', relationName),
http: {source: 'path'}}].concat(accepts), required: true,
description: 'Add a related item by id for ' + relationName + '.', 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', accessType: 'WRITE',
returns: {arg: relationName, type: modelThrough.modelName, root: true} returns: {arg: relationName, type: modelThrough.modelName, root: true}
}, addFunc); }, addFunc);
@ -568,10 +690,16 @@ module.exports = function(registry) {
define('__unlink__' + relationName, { define('__unlink__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'}, http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: this._removeOptionsArgIfDisabled([
description: 'Foreign key for ' + relationName, required: true, {
http: {source: 'path'}}, arg: 'fk', type: 'any',
description: 'Remove the ' + relationName + ' relation to an item by id.', 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', accessType: 'WRITE',
returns: [] returns: []
}, removeFunc); }, removeFunc);
@ -582,10 +710,16 @@ module.exports = function(registry) {
define('__exists__' + relationName, { define('__exists__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'}, http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: this._removeOptionsArgIfDisabled([
description: 'Foreign key for ' + relationName, required: true, {
http: {source: 'path'}}, arg: 'fk', type: 'any',
description: 'Check the existence of ' + relationName + ' relation to an item by id.', 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', accessType: 'READ',
returns: {arg: 'exists', type: 'boolean', root: true}, returns: {arg: 'exists', type: 'boolean', root: true},
rest: { rest: {
@ -594,7 +728,7 @@ module.exports = function(registry) {
if (ctx.result === false) { if (ctx.result === false) {
var modelName = ctx.method.sharedClass.name; var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id'); 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); var error = new Error(msg);
error.statusCode = error.status = 404; error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND'; error.code = 'MODEL_NOT_FOUND';
@ -627,8 +761,11 @@ module.exports = function(registry) {
define('__get__' + scopeName, { define('__get__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName}, http: {verb: 'get', path: '/' + pathName},
accepts: {arg: 'filter', type: 'object'}, accepts: this._removeOptionsArgIfDisabled([
description: 'Queries ' + scopeName + ' of ' + this.modelName + '.', {arg: 'filter', type: 'object'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
description: format('Queries %s of %s.', scopeName, this.modelName),
accessType: 'READ', accessType: 'READ',
returns: {arg: scopeName, type: [toModelName], root: true} returns: {arg: scopeName, type: [toModelName], root: true}
}); });
@ -636,8 +773,16 @@ module.exports = function(registry) {
define('__create__' + scopeName, { define('__create__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'post', path: '/' + pathName}, http: {verb: 'post', path: '/' + pathName},
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, accepts: this._removeOptionsArgIfDisabled([
description: 'Creates a new instance in ' + scopeName + ' of this model.', {
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', accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true} returns: {arg: 'data', type: toModelName, root: true}
}); });
@ -645,21 +790,46 @@ module.exports = function(registry) {
define('__delete__' + scopeName, { define('__delete__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'delete', path: '/' + pathName}, http: {verb: 'delete', path: '/' + pathName},
description: 'Deletes all ' + scopeName + ' of this model.', accepts: this._removeOptionsArgIfDisabled([
accessType: 'WRITE' {
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, { define('__count__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName + '/count'}, http: {verb: 'get', path: '/' + pathName + '/count'},
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, accepts: this._removeOptionsArgIfDisabled([
description: 'Counts ' + scopeName + ' of ' + this.modelName + '.', {
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', accessType: 'READ',
returns: {arg: 'count', type: 'number'} 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. * Enabled deeply-nested queries of related models via REST API.
* *
@ -702,9 +872,9 @@ module.exports = function(registry) {
acceptArgs = [ acceptArgs = [
{ {
arg: paramName, type: 'any', http: { source: 'path' }, arg: paramName, type: 'any', http: { source: 'path' },
description: 'Foreign key for ' + relation.name + '.', description: format('Foreign key for %s.', relation.name),
required: true required: true,
} },
]; ];
} else { } else {
httpPath = pathName; httpPath = pathName;
@ -732,12 +902,12 @@ module.exports = function(registry) {
var getterFn = relation.modelFrom.prototype[getterName]; var getterFn = relation.modelFrom.prototype[getterName];
if (typeof getterFn !== 'function') { 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]; var nestedFn = relation.modelTo.prototype[method.name];
if (typeof nestedFn !== 'function') { 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 = {}; var opts = {};
@ -806,8 +976,8 @@ module.exports = function(registry) {
listenerTree.before = listenerTree.before || {}; listenerTree.before = listenerTree.before || {};
listenerTree.after = listenerTree.after || {}; listenerTree.after = listenerTree.after || {};
var beforeListeners = remotes.listenerTree.before[toModelName] || {}; var beforeListeners = listenerTree.before[toModelName] || {};
var afterListeners = remotes.listenerTree.after[toModelName] || {}; var afterListeners = listenerTree.after[toModelName] || {};
sharedClass.methods().forEach(function(method) { sharedClass.methods().forEach(function(method) {
var delegateTo = method.rest && method.rest.delegateTo; var delegateTo = method.rest && method.rest.delegateTo;
@ -830,12 +1000,53 @@ module.exports = function(registry) {
}); });
} else { } 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; 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 // setup the initial model
Model.setup(); Model.setup();

View File

@ -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. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var runtime = require('./runtime'); var runtime = require('./runtime');
var assert = require('assert'); var assert = require('assert');
var async = require('async'); var async = require('async');
@ -60,9 +66,10 @@ module.exports = function(registry) {
function throwNotAttached(modelName, methodName) { function throwNotAttached(modelName, methodName) {
throw new Error( throw new Error(
'Cannot call ' + modelName + '.' + methodName + '().' + g.f('Cannot call %s.%s().' +
' The ' + methodName + ' method has not been setup.' + ' The %s method has not been setup.' +
' The PersistedModel has not been correctly attached to a DataSource!' ' 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 modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id'); 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); var error = new Error(msg);
error.statusCode = error.status = 404; error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND'; error.code = 'MODEL_NOT_FOUND';
@ -106,22 +113,78 @@ module.exports = function(registry) {
* @param {Object} model Updated model instance. * @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'); throwNotAttached(this.modelName, 'upsert');
}; };
/** /**
* Find one record matching the optional `where` filter. The same as `find`, but limited to one object. * Update or insert a model instance based on the search criteria.
* Returns an object, not collection. * If there is a single instance retrieved, update the retrieved model.
* If not found, create the object using data provided as second argument. * Creates a new model if no model instances were found.
* * Returns an error if multiple instances are found.
* @param {Object} where Where clause, such as `{test: 'me'}` * * @param {Object} [where] `where` filter, like
* ```
* { key: val, key2: {gt: 'val2'}, ...}
* ```
* <br/>see * <br/>see
* [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods). * [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.
* <br/>See [Fields filter](http://docs.strongloop.com/display/LB/Fields+filter).
* @property {String|Object|Array} include See PersistedModel.include documentation.
* <br/>See [Include filter](http://docs.strongloop.com/display/LB/Include+filter).
* @property {Number} limit Maximum number of instances to return.
* <br/>See [Limit filter](http://docs.strongloop.com/display/LB/Limit+filter).
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
* <br/>See [Order filter](http://docs.strongloop.com/display/LB/Order+filter).
* @property {Number} skip Number of results to skip.
* <br/>See [Skip filter](http://docs.strongloop.com/display/LB/Skip+filter).
* @property {Object} where Where clause, like
* ```
* {where: {key: val, key2: {gt: val2}, ...}}
* ```
* <br/>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. * @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 {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 {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) { PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
@ -456,10 +519,45 @@ module.exports = function(registry) {
* @param {Object} instance Updated instance. * @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'); 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`. * 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. * @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
@ -528,6 +626,9 @@ module.exports = function(registry) {
var typeName = PersistedModel.modelName; var typeName = PersistedModel.modelName;
var options = PersistedModel.settings; var options = PersistedModel.settings;
// This is just for LB 2.x
options.replaceOnPUT = options.replaceOnPUT === true;
function setRemoting(scope, name, options) { function setRemoting(scope, name, options) {
var fn = scope[name]; var fn = scope[name];
fn._delegate = true; fn._delegate = true;
@ -538,24 +639,82 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'create', { setRemoting(PersistedModel, 'create', {
description: 'Create a new instance of the model and persist it into the data source.', description: 'Create a new instance of the model and persist it into the data source.',
accessType: 'WRITE', 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}, returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'post', path: '/'} http: {verb: 'post', path: '/'}
}); });
setRemoting(PersistedModel, 'upsert', { var upsertOptions = {
aliases: ['updateOrCreate'], aliases: ['patchOrCreate', 'updateOrCreate'],
description: 'Update an existing model instance or insert a new one into the data source.', description: 'Patch an existing model instance or insert a new one into the data source.',
accessType: 'WRITE', 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}, returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'put', path: '/'} 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: 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: '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', { setRemoting(PersistedModel, 'exists', {
description: 'Check whether a model instance exists in the data source.', description: 'Check whether a model instance exists in the data source.',
accessType: 'READ', 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'}, returns: {arg: 'exists', type: 'boolean'},
http: [ http: [
{verb: 'get', path: '/:id/exists'}, {verb: 'get', path: '/:id/exists'},
@ -584,23 +743,51 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'findById', { 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', accessType: 'READ',
accepts: [ accepts: this._removeOptionsArgIfDisabled([
{ arg: 'id', type: 'any', description: 'Model id', required: true, { arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}}, http: {source: 'path'}},
{ arg: 'filter', type: 'object', {arg: 'filter', type: 'object',
description: 'Filter defining fields and include'} 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}, returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/:id'}, http: {verb: 'get', path: '/:id'},
rest: {after: convertNullToNotFoundError} 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', { setRemoting(PersistedModel, 'find', {
description: 'Find all instances of the model matched by filter from the data source.', description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ', 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}, returns: {arg: 'data', type: [typeName], root: true},
http: {verb: 'get', path: '/'} http: {verb: 'get', path: '/'}
}); });
@ -608,7 +795,12 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'findOne', { setRemoting(PersistedModel, 'findOne', {
description: 'Find first instance of the model matched by filter from the data source.', description: 'Find first instance of the model matched by filter from the data source.',
accessType: 'READ', 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}, returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/findOne'}, http: {verb: 'get', path: '/findOne'},
rest: {after: convertNullToNotFoundError} rest: {after: convertNullToNotFoundError}
@ -617,7 +809,10 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'destroyAll', { setRemoting(PersistedModel, 'destroyAll', {
description: 'Delete all matching records.', description: 'Delete all matching records.',
accessType: 'WRITE', 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: { returns: {
arg: 'count', arg: 'count',
type: 'object', type: 'object',
@ -630,29 +825,38 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'updateAll', { setRemoting(PersistedModel, 'updateAll', {
aliases: ['update'], 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', accessType: 'WRITE',
accepts: [ accepts: this._removeOptionsArgIfDisabled([
{arg: 'where', type: 'object', http: {source: 'query'}, {arg: 'where', type: 'object', http: { source: 'query'},
description: 'Criteria to match model instances'}, 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'}, description: 'An object of model property name/value pairs'},
], {arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
returns: { returns: {
arg: 'count', arg: 'info',
description: 'The number of instances updated', description: 'Information related to the outcome of the operation',
type: 'object', type: {
root: true count: {
type: 'number',
description: 'The number of instances updated',
},
},
root: true,
}, },
http: {verb: 'post', path: '/update'} http: {verb: 'post', path: '/update'}
}); });
setRemoting(PersistedModel, 'deleteById', { setRemoting(PersistedModel, 'deleteById', {
aliases: ['destroyById', 'removeById'], 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', accessType: 'WRITE',
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, accepts: this._removeOptionsArgIfDisabled([
http: {source: 'path'}}, {arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
http: {verb: 'del', path: '/:id'}, http: {verb: 'del', path: '/:id'},
returns: {arg: 'count', type: 'object', root: true} returns: {arg: 'count', type: 'object', root: true}
}); });
@ -660,18 +864,35 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'count', { setRemoting(PersistedModel, 'count', {
description: 'Count instances of the model matched by where from the data source.', description: 'Count instances of the model matched by where from the data source.',
accessType: 'READ', 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'}, returns: {arg: 'count', type: 'number'},
http: {verb: 'get', path: '/count'} http: {verb: 'get', path: '/count'}
}); });
setRemoting(PersistedModel.prototype, 'updateAttributes', { var updateAttributesOptions = {
description: 'Update attributes for a model instance and persist it into the data source.', aliases: ['patchAttributes'],
description: 'Patch attributes for a model instance and persist it into the data source.',
accessType: 'WRITE', 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}, 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) { if (options.trackChanges || options.enableRemoteReplication) {
setRemoting(PersistedModel, 'diff', { setRemoting(PersistedModel, 'diff', {
@ -680,7 +901,7 @@ module.exports = function(registry) {
accepts: [ accepts: [
{arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'}, {arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'},
{arg: 'remoteChanges', type: 'array', description: 'an array of change objects', {arg: 'remoteChanges', type: 'array', description: 'an array of change objects',
http: {source: 'body'}} http: {source: 'body'}}
], ],
returns: {arg: 'result', type: 'object', root: true}, returns: {arg: 'result', type: 'object', root: true},
http: {verb: 'post', path: '/diff'} http: {verb: 'post', path: '/diff'}
@ -746,10 +967,9 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'updateLastChange', { setRemoting(PersistedModel, 'updateLastChange', {
description: [ description:
'Update the properties of the most recent change record', 'Update the properties of the most recent change record ' +
'kept for this instance.' 'kept for this instance.',
],
accessType: 'WRITE', accessType: 'WRITE',
accepts: [ accepts: [
{ {
@ -757,8 +977,8 @@ module.exports = function(registry) {
description: 'Model id' description: 'Model id'
}, },
{ {
arg: 'data', type: 'object', http: {source: 'body'}, arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
description: 'An object of Change property name/value pairs' description: 'An object of Change property name/value pairs',
}, },
], ],
returns: { arg: 'result', type: this.Change.modelName, root: true }, returns: { arg: 'result', type: this.Change.modelName, root: true },
@ -882,12 +1102,7 @@ module.exports = function(registry) {
PersistedModel.checkpoint = function(cb) { PersistedModel.checkpoint = function(cb) {
var Checkpoint = this.getChangeModel().getCheckpointModel(); var Checkpoint = this.getChangeModel().getCheckpointModel();
this.getSourceId(function(err, sourceId) { Checkpoint.bumpLastSeq(cb);
if (err) return cb(err);
Checkpoint.create({
sourceId: sourceId
}, cb);
});
}; };
/** /**
@ -908,7 +1123,7 @@ module.exports = function(registry) {
* *
* @param {Number} [since] Since this checkpoint * @param {Number} [since] Since this checkpoint
* @param {Model} targetModel Target this model class * @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 * @param {Object} [options.filter] Replicate models that match this filter
* @callback {Function} [callback] Callback function called with `(err, conflicts)` arguments. * @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). * @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 }; since = { source: since, target: since };
} }
if (typeof options === 'function') {
options = {};
}
options = options || {}; options = options || {};
var sourceModel = this; var sourceModel = this;
@ -1047,7 +1266,7 @@ module.exports = function(registry) {
function bulkUpdate(_updates, cb) { function bulkUpdate(_updates, cb) {
debug('\tstarting bulk update'); debug('\tstarting bulk update');
updates = _updates; updates = _updates;
targetModel.bulkUpdate(updates, function(err) { targetModel.bulkUpdate(updates, options, function(err) {
var conflicts = err && err.details && err.details.conflicts; var conflicts = err && err.details && err.details.conflicts;
if (conflicts && err.statusCode == 409) { if (conflicts && err.statusCode == 409) {
diff.conflicts = conflicts; diff.conflicts = conflicts;
@ -1131,7 +1350,7 @@ module.exports = function(registry) {
if (err) return cb(err); if (err) return cb(err);
if (!inst) { if (!inst) {
return cb && 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) { if (inst.toObject) {
update.data = inst.toObject(); update.data = inst.toObject();
@ -1161,15 +1380,28 @@ module.exports = function(registry) {
* **Note: this is not atomic** * **Note: this is not atomic**
* *
* @param {Array} updates An updates list, usually from [createUpdates()](#persistedmodel-createupdates). * @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. * @param {Function} callback Callback function.
*/ */
PersistedModel.bulkUpdate = function(updates, callback) { PersistedModel.bulkUpdate = function(updates, options, callback) {
var tasks = []; var tasks = [];
var Model = this; var Model = this;
var Change = this.getChangeModel(); var Change = this.getChangeModel();
var conflicts = []; 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) { buildLookupOfAffectedModelData(Model, updates, function(err, currentMap) {
if (err) return callback(err); if (err) return callback(err);
@ -1179,18 +1411,18 @@ module.exports = function(registry) {
switch (update.type) { switch (update.type) {
case Change.UPDATE: case Change.UPDATE:
tasks.push(function(cb) { 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; break;
case Change.CREATE: case Change.CREATE:
tasks.push(function(cb) { 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; break;
case Change.DELETE: case Change.DELETE:
tasks.push(function(cb) { tasks.push(function(cb) {
applyDelete(Model, id, current, update.change, conflicts, cb); applyDelete(Model, id, current, update.change, conflicts, options, cb);
}); });
break; break;
} }
@ -1199,7 +1431,7 @@ module.exports = function(registry) {
async.parallel(tasks, function(err) { async.parallel(tasks, function(err) {
if (err) return callback(err); if (err) return callback(err);
if (conflicts.length) { if (conflicts.length) {
err = new Error('Conflict'); err = new Error(g.f('Conflict'));
err.statusCode = 409; err.statusCode = 409;
err.details = { conflicts: conflicts }; err.details = { conflicts: conflicts };
return callback(err); 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 Change = Model.getChangeModel();
var rev = current ? Change.revisionForInst(current) : null; var rev = current ? Change.revisionForInst(current) : null;
@ -1242,7 +1474,7 @@ module.exports = function(registry) {
// but not included in `data` // but not included in `data`
// See https://github.com/strongloop/loopback/issues/1215 // 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); if (err) return cb(err);
var count = result && result.count; var count = result && result.count;
@ -1263,22 +1495,22 @@ module.exports = function(registry) {
case undefined: case undefined:
case null: case null:
return cb(new Error( return cb(new Error(
'Cannot apply bulk updates, ' + g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' + 'the connector does not correctly report ' +
'the number of updated records.')); 'the number of updated records.')));
default: default:
debug('%s.updateAll modified unexpected number of instances: %j', debug('%s.updateAll modified unexpected number of instances: %j',
Model.modelName, count); Model.modelName, count);
return cb(new Error( return cb(new Error(
'Bulk update failed, the connector has modified unexpected ' + g.f('Bulk update failed, the connector has modified unexpected ' +
'number of records: ' + JSON.stringify(count))); 'number of records: %s', JSON.stringify(count))));
} }
}); });
} }
function applyCreate(Model, id, current, data, change, conflicts, cb) { function applyCreate(Model, id, current, data, change, conflicts, options, cb) {
Model.create(data, function(createErr) { Model.create(data, options, function(createErr) {
if (!createErr) return cb(); if (!createErr) return cb();
// We don't have a reliable way how to detect the situation // 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) { if (!current) {
// The instance was either already deleted or not created at all, // The instance was either already deleted or not created at all,
// we are done. // we are done.
@ -1324,7 +1556,7 @@ module.exports = function(registry) {
return Change.rectifyModelChanges(Model.modelName, [id], cb); 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); if (err) return cb(err);
var count = result && result.count; var count = result && result.count;
@ -1345,16 +1577,16 @@ module.exports = function(registry) {
case undefined: case undefined:
case null: case null:
return cb(new Error( return cb(new Error(
'Cannot apply bulk updates, ' + g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' + 'the connector does not correctly report ' +
'the number of deleted records.')); 'the number of deleted records.')));
default: default:
debug('%s.deleteAll modified unexpected number of instances: %j', debug('%s.deleteAll modified unexpected number of instances: %j',
Model.modelName, count); Model.modelName, count);
return cb(new Error( return cb(new Error(
'Bulk update failed, the connector has deleted unexpected ' + g.f('Bulk update failed, the connector has deleted unexpected ' +
'number of records: ' + JSON.stringify(count))); 'number of records: %s', JSON.stringify(count))));
} }
}); });
} }
@ -1413,14 +1645,16 @@ module.exports = function(registry) {
var idDefn = idProp && idProp.defaultFn; var idDefn = idProp && idProp.defaultFn;
if (idType !== String || !(idDefn === 'uuid' || idDefn === 'guid')) { if (idType !== String || !(idDefn === 'uuid' || idDefn === 'guid')) {
deprecated('The model ' + this.modelName + ' is tracking changes, ' + 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 save', rectifyOnSave);
Model.observe('after delete', rectifyOnDelete); 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 // initial cleanup
cleanup(); cleanup();
@ -1449,7 +1683,7 @@ module.exports = function(registry) {
ctx.instance, ctx.currentInstance, ctx.where, ctx.data); ctx.instance, ctx.currentInstance, ctx.where, ctx.data);
} }
if (id) { if (id != null) {
ctx.Model.rectifyChange(id, reportErrorAndNext); ctx.Model.rectifyChange(id, reportErrorAndNext);
} else { } else {
ctx.Model.rectifyAllChanges(reportErrorAndNext); ctx.Model.rectifyAllChanges(reportErrorAndNext);
@ -1473,7 +1707,7 @@ module.exports = function(registry) {
debug('context instance:%j where:%j', ctx.instance, ctx.where); debug('context instance:%j where:%j', ctx.instance, ctx.where);
} }
if (id) { if (id != null) {
ctx.Model.rectifyChange(id, reportErrorAndNext); ctx.Model.rectifyChange(id, reportErrorAndNext);
} else { } else {
ctx.Model.rectifyAllChanges(reportErrorAndNext); ctx.Model.rectifyAllChanges(reportErrorAndNext);
@ -1569,8 +1803,8 @@ module.exports = function(registry) {
this.findLastChange(id, function(err, inst) { this.findLastChange(id, function(err, inst) {
if (err) return cb(err); if (err) return cb(err);
if (!inst) { if (!inst) {
err = new Error('No change record found for ' + err = new Error(g.f('No change record found for %s with id %s',
self.modelName + ' with id ' + id); self.modelName, id));
err.statusCode = 404; err.statusCode = 404;
return cb(err); return cb(err);
} }

View File

@ -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 assert = require('assert');
var extend = require('util')._extend; var extend = require('util')._extend;
var juggler = require('loopback-datasource-juggler'); var juggler = require('loopback-datasource-juggler');
@ -108,11 +114,11 @@ Registry.prototype.createModel = function(name, properties, options) {
if (BaseModel === undefined) { if (BaseModel === undefined) {
if (baseName === 'DataModel') { if (baseName === 'DataModel') {
console.warn('Model `%s` is extending deprecated `DataModel. ' + g.warn('Model `%s` is extending deprecated `DataModel. ' +
'Use `PersistedModel` instead.', name); 'Use `PersistedModel` instead.', name);
BaseModel = this.getModel('PersistedModel'); BaseModel = this.getModel('PersistedModel');
} else { } 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); '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]); relations[key] = extend(relations[key] || {}, config.relations[key]);
}); });
} else if (config.relations != null) { } 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); 'must be an object', modelName);
} }
@ -203,7 +209,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
addACL(acls, acl); addACL(acls, acl);
}); });
} else if (config.acls != null) { } 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); 'must be an array of objects', modelName);
} }
@ -220,12 +226,12 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
if (!(p in excludedProperties)) { if (!(p in excludedProperties)) {
settings[p] = config.options[p]; settings[p] = config.options[p];
} else { } else {
console.warn('Property `%s` cannot be reconfigured for `%s`', g.warn('Property `%s` cannot be reconfigured for `%s`',
p, modelName); p, modelName);
} }
} }
} else if (config.options != null) { } 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); 'must be an object', modelName);
} }
@ -244,8 +250,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
} else { } else {
debug('Model `%s` is not attached to any DataSource, possibly by a mistake.', debug('Model `%s` is not attached to any DataSource, possibly by a mistake.',
modelName); modelName);
console.warn( g.warn(
'The configuration of `%s` is missing `dataSource` property.\n' + 'The configuration of `%s` is missing {{`dataSource`}} property.\n' +
'Use `null` or `false` to mark models not attached to any data source.', 'Use `null` or `false` to mark models not attached to any data source.',
modelName); modelName);
} }
@ -257,7 +263,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) { Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
if (!methods) return; if (!methods) return;
if (typeof methods !== 'object') { if (typeof methods !== 'object') {
console.warn('Ignoring non-object "methods" setting of "%s".', g.warn('Ignoring non-object "methods" setting of "%s".',
ModelCtor.modelName); ModelCtor.modelName);
return; return;
} }
@ -265,11 +271,11 @@ Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
Object.keys(methods).forEach(function(key) { Object.keys(methods).forEach(function(key) {
var meta = methods[key]; var meta = methods[key];
if (typeof meta.isStatic !== 'boolean') { 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.', 'flag, the method is registered as an instance method.',
ModelCtor.modelName, ModelCtor.modelName,
key); 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); ModelCtor.remoteMethod(key, meta);
}); });
@ -301,7 +307,7 @@ Registry.prototype.getModel = function(modelName) {
var model = this.findModel(modelName); var model = this.findModel(modelName);
if (model) return model; if (model) return model;
throw new Error('Model not found: ' + modelName); throw new Error(g.f('Model not found: %s', modelName));
}; };
/** /**

View File

@ -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. * This is an internal file that should not be used outside of loopback.
* All exported entities can be accessed via the `loopback` object. * All exported entities can be accessed via the `loopback` object.

View File

@ -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 assert = require('assert');
var express = require('express'); var express = require('express');
var merge = require('util')._extend; var merge = require('util')._extend;
@ -183,7 +190,7 @@ proto.middleware = function(name, paths, handler) {
} }
if (this._requestHandlingPhases.indexOf(name) === -1) 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); debug('use %s %s %s', fullPhaseName, paths, handlerName);

View File

@ -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; exports.createPromiseCallback = createPromiseCallback;
function createPromiseCallback() { function createPromiseCallback() {

View File

@ -1,6 +1,9 @@
{ {
"name": "loopback", "name": "loopback",
"version": "2.26.2", "version": "2.41.1",
"publishConfig": {
"tag": "lts"
},
"description": "LoopBack: Open Source Framework for Node.js", "description": "LoopBack: Open Source Framework for Node.js",
"homepage": "http://loopback.io", "homepage": "http://loopback.io",
"keywords": [ "keywords": [
@ -29,64 +32,78 @@
"mBaaS" "mBaaS"
], ],
"scripts": { "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": { "dependencies": {
"async": "^0.9.0", "async": "^2.0.1",
"bcryptjs": "^2.1.0", "bcryptjs": "^2.1.0",
"body-parser": "^1.12.0", "body-parser": "^1.12.0",
"canonical-json": "0.0.4", "canonical-json": "0.0.4",
"continuation-local-storage": "^3.1.3",
"cookie-parser": "^1.3.4", "cookie-parser": "^1.3.4",
"debug": "^2.1.2", "debug": "^2.1.2",
"depd": "^1.0.0", "depd": "^1.0.0",
"ejs": "^2.3.1", "ejs": "^2.3.1",
"errorhandler": "^1.3.4", "errorhandler": "^1.3.4",
"express": "^4.12.2", "express": "^4.16.2",
"inflection": "^1.6.0", "inflection": "^1.6.0",
"isemail": "^1.2.0",
"loopback-connector-remote": "^1.0.3", "loopback-connector-remote": "^1.0.3",
"loopback-context": "^1.0.0",
"loopback-phase": "^1.2.0", "loopback-phase": "^1.2.0",
"nodemailer": "^1.3.1", "nodemailer": "^2.5.0",
"nodemailer-stub-transport": "^0.1.5", "nodemailer-stub-transport": "^1.0.0",
"serve-favicon": "^2.2.0", "serve-favicon": "^2.2.0",
"stable": "^0.1.5", "stable": "^0.1.5",
"strong-globalize": "^2.6.2",
"strong-remoting": "^2.21.0", "strong-remoting": "^2.21.0",
"uid2": "0.0.3", "uid2": "0.0.3",
"underscore.string": "^3.0.3" "underscore.string": "^3.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"loopback-datasource-juggler": "^2.19.0" "loopback-datasource-juggler": "^2.56.0"
}, },
"devDependencies": { "devDependencies": {
"bluebird": "^2.9.9", "babel-preset-es2015": "^6.24.1",
"browserify": "^10.0.0", "babelify": "^7.3.0",
"chai": "^2.1.1", "bluebird": "^3.4.1",
"browserify": "^13.1.0",
"chai": "^3.5.0",
"coveralls": "^2.11.15",
"es5-shim": "^4.1.0", "es5-shim": "^4.1.0",
"grunt": "^0.4.5", "express-session": "^1.14.0",
"grunt-browserify": "^3.5.0", "grunt": "^1.0.1",
"grunt-cli": "^0.1.13", "grunt-browserify": "^5.0.0",
"grunt-contrib-jshint": "^0.11.0", "grunt-cli": "^1.2.0",
"grunt-contrib-uglify": "^0.9.1", "grunt-contrib-jshint": "^1.0.0",
"grunt-contrib-watch": "^0.6.1", "grunt-contrib-uglify": "^2.0.0",
"grunt-jscs": "^1.5.0", "grunt-contrib-watch": "^1.0.0",
"grunt-karma": "^0.10.1", "grunt-jscs": "^3.0.1",
"grunt-karma": "^2.0.0",
"grunt-mocha-test": "^0.12.7", "grunt-mocha-test": "^0.12.7",
"karma": "^0.12.31", "karma": "^1.1.2",
"karma-browserify": "^4.0.0", "karma-browserify": "^5.0.5",
"karma-chrome-launcher": "^0.1.7", "karma-chrome-launcher": "^1.0.1",
"karma-firefox-launcher": "^0.1.4", "karma-firefox-launcher": "^1.0.0",
"karma-html2js-preprocessor": "^0.1.0", "karma-html2js-preprocessor": "^1.0.0",
"karma-junit-reporter": "^0.2.2", "karma-junit-reporter": "^1.0.0",
"karma-mocha": "^0.1.10", "karma-mocha": "^1.1.1",
"karma-phantomjs-launcher": "^0.1.4", "karma-phantomjs-launcher": "^1.0.0",
"karma-script-launcher": "^0.1.0", "karma-script-launcher": "^1.0.0",
"loopback-boot": "^2.7.0", "loopback-boot": "^2.7.0",
"loopback-datasource-juggler": "^2.19.1", "loopback-datasource-juggler": "^2.56.0",
"loopback-testing": "~1.1.0", "loopback-testing": "^1.4.0",
"mocha": "^2.1.0", "mocha": "^3.0.0",
"nyc": "^10.1.2",
"phantomjs-prebuilt": "^2.1.7",
"sinon": "^1.13.0", "sinon": "^1.13.0",
"sinon-chai": "^2.8.0",
"strong-task-emitter": "^0.0.6", "strong-task-emitter": "^0.0.6",
"supertest": "^0.15.0" "supertest": "^2.0.0",
"supertest-as-promised": "^4.0.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -95,15 +112,22 @@
"browser": { "browser": {
"express": "./lib/browser-express.js", "express": "./lib/browser-express.js",
"./lib/server-app.js": "./lib/browser-express.js", "./lib/server-app.js": "./lib/browser-express.js",
"./server/current-context.js": "./browser/current-context.js",
"connect": false, "connect": false,
"nodemailer": false, "nodemailer": false,
"supertest": false, "supertest": false,
"depd": "loopback-datasource-juggler/lib/browser.depd.js", "depd": "loopback-datasource-juggler/lib/browser.depd.js",
"bcrypt": false "bcrypt": false
}, },
"license": "MIT", "config": {
"optionalDependencies": { "ci": {
"sl-blip": "http://blip.strongloop.com/loopback@2.26.2" "debug": "*,-mocha:*,-eslint:*"
} }
},
"ci": {
"downstreamIgnoreList": [
"dashboard-controller",
"gateway-director-management-interface"
]
},
"license": "MIT"
} }

View File

@ -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;
}
}
};

View File

@ -1,52 +1,15 @@
var loopback = require('../../lib/loopback'); // 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 = context; var deprecated = require('depd')('loopback');
var g = require('strong-globalize')();
var perRequestContext = require('loopback-context').perRequest;
var name = 'loopback'; module.exports = function() {
deprecated(g.f('%s middleware is deprecated. See %s for more details.',
/** 'loopback#context',
* Context middleware. 'https://docs.strongloop.com/display/APIC/Using%20current%20context'));
* ```js return perRequestContext.apply(this, arguments);
* 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();
});
};
}

View File

@ -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'); var expressErrorHandler = require('errorhandler');
expressErrorHandler.title = 'Loopback'; expressErrorHandler.title = 'Loopback';

View File

@ -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. * Serve the LoopBack favicon.
* @header 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);
};

View File

@ -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. * Module dependencies.
*/ */
@ -68,8 +73,21 @@ function rest() {
if (handlers.length === 1) { if (handlers.length === 1) {
return handlers[0](req, res, next); return handlers[0](req, res, next);
} }
async.eachSeries(handlers, function(handler, done) {
handler(req, res, done); executeHandlers(handlers, req, res, next);
}, 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);
}
}
}

View File

@ -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. * Serve static assets of a LoopBack application.
* *

View File

@ -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. * Export the middleware.
*/ */

View File

@ -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. * Module dependencies.
*/ */
'use strict';
var g = require('strong-globalize')();
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var assert = require('assert'); var assert = require('assert');
var debug = require('debug')('loopback:middleware:token'); 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 * Rewrite the url to replace current user literal with the logged in user id
*/ */
function rewriteUserLiteral(req, currentUserLiteral) { function rewriteUserLiteral(req, currentUserLiteral, next) {
if (req.accessToken && req.accessToken.userId && currentUserLiteral) { if (!currentUserLiteral) return next();
var literalRegExp = new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g');
if (req.accessToken && req.accessToken.userId) {
// Replace /me/ with /current-user-id/ // Replace /me/ with /current-user-id/
var urlBeforeRewrite = req.url; var urlBeforeRewrite = req.url;
req.url = req.url.replace( req.url = req.url.replace(literalRegExp,
new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'),
'/' + req.accessToken.userId + '$1'); '/' + req.accessToken.userId + '$1');
if (req.url !== urlBeforeRewrite) { if (req.url !== urlBeforeRewrite) {
debug('req.url has been rewritten from %s to %s', urlBeforeRewrite, debug('req.url has been rewritten from %s to %s', urlBeforeRewrite,
req.url); 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) { function escapeRegExp(str) {
@ -62,6 +84,8 @@ function escapeRegExp(str) {
* @property {Array} [headers] Array of header names. * @property {Array} [headers] Array of header names.
* @property {Array} [params] Array of param names. * @property {Array} [params] Array of param names.
* @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request * @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 {Function|String} [model] AccessToken model name or class to use.
* @property {String} [currentUserLiteral] String literal for the current user. * @property {String} [currentUserLiteral] String literal for the current user.
* @header loopback.token([options]) * @header loopback.token([options])
@ -80,6 +104,9 @@ function token(options) {
currentUserLiteral = escapeRegExp(currentUserLiteral); currentUserLiteral = escapeRegExp(currentUserLiteral);
} }
var enableDoublecheck = !!options.enableDoublecheck;
var overwriteExistingToken = !!options.overwriteExistingToken;
return function(req, res, next) { return function(req, res, next) {
var app = req.app; var app = req.app;
var registry = app.registry; var registry = app.registry;
@ -97,15 +124,27 @@ function token(options) {
'loopback.token() middleware requires a AccessToken model'); 'loopback.token() middleware requires a AccessToken model');
if (req.accessToken !== undefined) { if (req.accessToken !== undefined) {
rewriteUserLiteral(req, currentUserLiteral); if (!enableDoublecheck) {
return next(); // 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) { TokenModel.findForRequest(req, options, function(err, token) {
req.accessToken = token || null; req.accessToken = token || null;
rewriteUserLiteral(req, currentUserLiteral);
var ctx = loopback.getCurrentContext(); var ctx = req.loopbackContext;
if (ctx) ctx.set('accessToken', token); if (ctx && ctx.active) ctx.set('accessToken', token);
next(err);
if (err) return next(err);
rewriteUserLiteral(req, currentUserLiteral, next);
}); });
}; };
} }

View File

@ -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. * Export the middleware.
* See discussion in Connect pull request #954 for more details * See discussion in Connect pull request #954 for more details

View File

@ -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 */ /*jshint -W030 */
var loopback = require('../'); 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'); var debug = require('debug')('loopback:test:access-control.integration');
describe('access control - integration', function() { describe('access control - integration', function() {
before(function(done) {
if (app.booting) {
return app.once('booted', done);
}
done();
});
lt.beforeEach.withApp(app); lt.beforeEach.withApp(app);
lt.beforeEach.withUserModel('user');
/* /*
describe('accessToken', function() { describe('accessToken', function() {
@ -94,7 +106,7 @@ describe('access control - integration', function() {
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
beforeEach(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.describe.whenCalledRemotely('DELETE', '/api/users/:id', function() {
lt.it.shouldBeAllowed(); lt.it.shouldBeAllowed();
@ -110,11 +122,21 @@ describe('access control - integration', function() {
assert.equal(user.password, undefined); 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.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
lt.it.shouldBeAllowed(); 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.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser); lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser);
@ -126,6 +148,7 @@ describe('access control - integration', function() {
var userCounter; var userCounter;
function newUserData() { function newUserData() {
userCounter = userCounter ? ++userCounter : 1; userCounter = userCounter ? ++userCounter : 1;
return { return {
email: 'new-' + userCounter + '@test.test', email: 'new-' + userCounter + '@test.test',
password: 'test' password: 'test'
@ -134,6 +157,34 @@ describe('access control - integration', function() {
}); });
describe('/banks', 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.beforeEach.givenModel('bank');
lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks'); lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks');
@ -155,13 +206,18 @@ describe('access control - integration', function() {
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank); lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank); lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, '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() { function urlForBank() {
return '/api/banks/' + this.bank.id; return '/api/banks/' + this.bank.id;
} }
}); });
describe('/accounts', function() { describe('/accounts with replaceOnPUT true', function() {
var count = 0; var count = 0;
before(function() { before(function() {
var roleModel = loopback.getModelByType(loopback.Role); var roleModel = loopback.getModelByType(loopback.Role);
@ -175,47 +231,68 @@ describe('access control - integration', function() {
}); });
}); });
lt.beforeEach.givenModel('account'); lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts'); 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.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount); lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, '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() { lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
var actId;
beforeEach(function(done) { beforeEach(function(done) {
var self = this; var self = this;
// Create an account under the given user // Create an account under the given user
app.models.account.create({ app.models.accountWithReplaceOnPUTtrue.create({
userId: self.user.id, userId: self.loggedInAccessToken.userId,
balance: 100 balance: 100
}, function(err, act) { }, function(err, act) {
self.url = '/api/accounts/' + act.id; actId = act.id;
self.url = '/api/accounts-replacing/' + actId;
done(); done();
}); });
});
}); lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() {
lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
lt.it.shouldBeAllowed(); lt.it.shouldBeAllowed();
}); });
lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() { lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeAllowed(); lt.it.shouldBeAllowed();
}); });
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() { lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeDenied(); 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.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
@ -223,7 +300,77 @@ describe('access control - integration', function() {
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
function urlForAccount() { function urlForAccount() {
return '/api/accounts/' + this.account.id; 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-updating/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('GET', '/api/accounts-updating/:id', function() {
lt.it.shouldBeAllowed();
});
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);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
function urlForAccount() {
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id;
}
function urlForReplaceAccountPOST() {
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace';
} }
}); });

View File

@ -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 loopback = require('../');
var extend = require('util')._extend; var extend = require('util')._extend;
var session = require('express-session');
var Token = loopback.AccessToken.extend('MyToken'); var Token = loopback.AccessToken.extend('MyToken');
var ds = loopback.createDataSource({connector: loopback.Memory}); var ds = loopback.createDataSource({connector: loopback.Memory});
Token.attachTo(ds); Token.attachTo(ds);
@ -65,7 +73,7 @@ describe('loopback.token(options)', function() {
.end(done); .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) { it('parses "standalone-token"', function(done) {
var token = this.token.id; var token = this.token.id;
token = 'Basic ' + new Buffer(token).toString('base64'); token = 'Basic ' + new Buffer(token).toString('base64');
@ -144,7 +152,8 @@ describe('loopback.token(options)', function() {
.set('authorization', id) .set('authorization', id)
.end(function(err, res) { .end(function(err, res) {
assert(!err); assert(!err);
assert.deepEqual(res.body, {userId: userId}); assert.deepEqual(res.body, { userId: userId });
done(); done();
}); });
}); });
@ -159,7 +168,8 @@ describe('loopback.token(options)', function() {
.set('authorization', id) .set('authorization', id)
.end(function(err, res) { .end(function(err, res) {
assert(!err); assert(!err);
assert.deepEqual(res.body, {userId: userId, state: 1}); assert.deepEqual(res.body, { userId: userId, state: 1 });
done(); done();
}); });
}); });
@ -174,15 +184,47 @@ describe('loopback.token(options)', function() {
.set('authorization', id) .set('authorization', id)
.end(function(err, res) { .end(function(err, res) {
assert(!err); assert(!err);
assert.deepEqual(res.body, {userId: userId, state: 1}); assert.deepEqual(res.body, { userId: userId, state: 1 });
done(); 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) { it('should skip when req.token is already present', function(done) {
var tokenStub = { id: 'stub id' }; var tokenStub = { id: 'stub id' };
app.use(function(req, res, next) { app.use(function(req, res, next) {
req.accessToken = tokenStub; req.accessToken = tokenStub;
next(); next();
}); });
app.use(loopback.token({ model: Token })); app.use(loopback.token({ model: Token }));
@ -195,10 +237,139 @@ describe('loopback.token(options)', function() {
.expect(200) .expect(200)
.end(function(err, res) { .end(function(err, res) {
if (err) return done(err); if (err) return done(err);
expect(res.body).to.eql(tokenStub); expect(res.body).to.eql(tokenStub);
done(); 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() { describe('AccessToken', function() {
@ -214,10 +385,38 @@ describe('AccessToken', function() {
assert(Object.prototype.toString.call(this.token.created), '[object Date]'); assert(Object.prototype.toString.call(this.token.created), '[object Date]');
}); });
it('should be validateable', function(done) { describe('.validate()', function() {
this.token.validate(function(err, isValid) { it('accepts valid tokens', function(done) {
assert(isValid); this.token.validate(function(err, isValid) {
done(); 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) { Token.findForRequest(req, function(err, token) {
if (err) return done(err); if (err) return done(err);
expect(token.id).to.eql(expectedTokenId); expect(token.id).to.eql(expectedTokenId);
done(); done();
}); });
}); });
@ -255,6 +456,9 @@ describe('AccessToken', function() {
}); });
describe('app.enableAuth()', function() { describe('app.enableAuth()', function() {
beforeEach(function setupAuthWithModels() {
app.enableAuth({ dataSource: ds });
});
beforeEach(createTestingToken); beforeEach(createTestingToken);
it('prevents remote call with 401 status on denied ACL', function(done) { it('prevents remote call with 401 status on denied ACL', function(done) {
@ -266,15 +470,17 @@ describe('app.enableAuth()', function() {
if (err) { if (err) {
return done(err); return done(err);
} }
var errorResponse = res.body.error; var errorResponse = res.body.error;
assert(errorResponse); assert(errorResponse);
assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED'); assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED');
done(); done();
}); });
}); });
it('prevent remote call with app setting status on denied ACL', function(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') .del('/tests/123')
.expect(403) .expect(403)
.set('authorization', this.token.id) .set('authorization', this.token.id)
@ -282,15 +488,17 @@ describe('app.enableAuth()', function() {
if (err) { if (err) {
return done(err); return done(err);
} }
var errorResponse = res.body.error; var errorResponse = res.body.error;
assert(errorResponse); assert(errorResponse);
assert.equal(errorResponse.code, 'ACCESS_DENIED'); assert.equal(errorResponse.code, 'ACCESS_DENIED');
done(); done();
}); });
}); });
it('prevent remote call with app setting status on denied ACL', function(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') .del('/tests/123')
.expect(404) .expect(404)
.set('authorization', this.token.id) .set('authorization', this.token.id)
@ -298,9 +506,11 @@ describe('app.enableAuth()', function() {
if (err) { if (err) {
return done(err); return done(err);
} }
var errorResponse = res.body.error; var errorResponse = res.body.error;
assert(errorResponse); assert(errorResponse);
assert.equal(errorResponse.code, 'MODEL_NOT_FOUND'); assert.equal(errorResponse.code, 'MODEL_NOT_FOUND');
done(); done();
}); });
}); });
@ -314,9 +524,11 @@ describe('app.enableAuth()', function() {
if (err) { if (err) {
return done(err); return done(err);
} }
var errorResponse = res.body.error; var errorResponse = res.body.error;
assert(errorResponse); assert(errorResponse);
assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED'); assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED');
done(); done();
}); });
}); });
@ -324,7 +536,8 @@ describe('app.enableAuth()', function() {
it('stores token in the context', function(done) { it('stores token in the context', function(done) {
var TestModel = loopback.createModel('TestModel', { base: 'Model' }); var TestModel = loopback.createModel('TestModel', { base: 'Model' });
TestModel.getToken = function(cb) { 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', { TestModel.remoteMethod('getToken', {
returns: { arg: 'token', type: 'object' }, returns: { arg: 'token', type: 'object' },
@ -347,17 +560,44 @@ describe('app.enableAuth()', function() {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.end(function(err, res) { .end(function(err, res) {
if (err) return done(err); if (err) return done(err);
expect(res.body.token.id).to.eql(token.id); expect(res.body.token.id).to.eql(token.id);
done(); 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) { function createTestingToken(done) {
var test = this; var test = this;
Token.create({userId: '123'}, function(err, token) { Token.create({userId: '123'}, function(err, token) {
if (err) return done(err); if (err) return done(err);
test.token = token; test.token = token;
done(); done();
}); });
} }
@ -380,8 +620,9 @@ function createTestApp(testToken, settings, done) {
}, settings.token); }, settings.token);
var app = loopback(); var app = loopback();
app.set('logoutSessionsOnSensitiveChanges', true);
app.use(loopback.cookieParser('secret')); app.use(cookieParser('secret'));
app.use(loopback.token(tokenSettings)); app.use(loopback.token(tokenSettings));
app.get('/token', function(req, res) { app.get('/token', function(req, res) {
res.cookie('authorization', testToken.id, {signed: true}); res.cookie('authorization', testToken.id, {signed: true});
@ -439,3 +680,17 @@ function createTestApp(testToken, settings, done) {
return app; 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;
}

View File

@ -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 assert = require('assert');
var loopback = require('../index'); var loopback = require('../index');
var Scope = loopback.Scope; var Scope = loopback.Scope;
@ -358,19 +363,17 @@ describe('security ACLs', function() {
}); });
describe('access check', 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) { 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 checkAccessCalled = false;
var beforeHookCalled = 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 // fake / spy on the checkAccess method
MyTestModel.checkAccess = function() { MyTestModel.checkAccess = function() {
var cb = arguments[arguments.length - 1]; var cb = arguments[arguments.length - 1];
@ -382,7 +385,9 @@ describe('access check', function() {
MyTestModel.beforeRemote('find', function(ctx, next) { MyTestModel.beforeRemote('find', function(ctx, next) {
// ensure this is called after checkAccess // ensure this is called after checkAccess
if (!checkAccessCalled) return done(new Error('incorrect order')); if (!checkAccessCalled) return done(new Error('incorrect order'));
beforeHookCalled = true; beforeHookCalled = true;
next(); next();
}); });
@ -391,6 +396,7 @@ describe('access check', function() {
.end(function(err, result) { .end(function(err, result) {
assert(beforeHookCalled, 'the before hook should be called'); assert(beforeHookCalled, 'the before hook should be called');
assert(checkAccessCalled, 'checkAccess should have been called'); assert(checkAccessCalled, 'checkAccess should have been called');
done(); done();
}); });
}); });

View File

@ -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 */ /*jshint -W030 */
var async = require('async'); var async = require('async');
@ -9,6 +14,7 @@ var loopback = require('../');
var PersistedModel = loopback.PersistedModel; var PersistedModel = loopback.PersistedModel;
var describe = require('./util/describe'); var describe = require('./util/describe');
var expect = require('chai').expect;
var it = require('./util/it'); var it = require('./util/it');
describe('app', function() { describe('app', function() {
@ -34,10 +40,12 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql([ expect(steps).to.eql([
'initial', 'session', 'auth', 'parse', 'initial', 'session', 'auth', 'parse',
'main', 'routes', 'files', 'final' 'main', 'routes', 'files', 'final'
]); ]);
done(); done();
}); });
}); });
@ -48,7 +56,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['first', 'second']); expect(steps).to.eql(['first', 'second']);
done(); done();
}); });
}); });
@ -60,7 +70,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['routes:before', 'main', 'routes:after']); expect(steps).to.eql(['routes:before', 'main', 'routes:after']);
done(); done();
}); });
}); });
@ -80,7 +92,9 @@ describe('app', function() {
expect(found).have.property('phase', 'routes:before'); expect(found).have.property('phase', 'routes:before');
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['my-handler', 'extra-handler']); expect(steps).to.eql(['my-handler', 'extra-handler']);
done(); done();
}); });
}); });
@ -98,7 +112,9 @@ describe('app', function() {
expect(found).have.property('phase', 'routes:before'); expect(found).have.property('phase', 'routes:before');
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['my-handler']); expect(steps).to.eql(['my-handler']);
done(); done();
}); });
}); });
@ -116,7 +132,9 @@ describe('app', function() {
expect(found).have.property('phase', 'routes:before'); expect(found).have.property('phase', 'routes:before');
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['my-handler']); expect(steps).to.eql(['my-handler']);
done(); done();
}); });
}); });
@ -126,6 +144,7 @@ describe('app', function() {
app.middleware('initial', function(req, res, next) { app.middleware('initial', function(req, res, next) {
steps.push('initial'); steps.push('initial');
next(expectedError); next(expectedError);
}); });
@ -133,12 +152,15 @@ describe('app', function() {
app.use(function errorHandler(err, req, res, next) { app.use(function errorHandler(err, req, res, next) {
expect(err).to.equal(expectedError); expect(err).to.equal(expectedError);
steps.push('error'); steps.push('error');
next(); next();
}); });
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['initial', 'error']); expect(steps).to.eql(['initial', 'error']);
done(); done();
}); });
}); });
@ -152,6 +174,7 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
expect(err).to.equal(expectedError); expect(err).to.equal(expectedError);
done(); done();
}); });
}); });
@ -170,12 +193,15 @@ describe('app', function() {
app.middleware('initial', function(err, req, res, next) { app.middleware('initial', function(err, req, res, next) {
handledError = err; handledError = err;
next(); next();
}); });
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(handledError).to.equal(expectedError); expect(handledError).to.equal(expectedError);
done(); done();
}); });
}); });
@ -188,7 +214,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) { function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['/scope', '/scope/item']); expect(steps).to.eql(['/scope', '/scope/item']);
done(); done();
}); });
}); });
@ -201,7 +229,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) { function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['/a', '/b']); expect(steps).to.eql(['/a', '/b']);
done(); done();
}); });
}); });
@ -214,7 +244,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) { function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['/a', '/b', '/scope']); expect(steps).to.eql(['/a', '/b', '/scope']);
done(); done();
}); });
}); });
@ -222,12 +254,15 @@ describe('app', function() {
it('sets req.url to a sub-path', function(done) { it('sets req.url to a sub-path', function(done) {
app.middleware('initial', ['/scope'], function(req, res, next) { app.middleware('initial', ['/scope'], function(req, res, next) {
steps.push(req.url); steps.push(req.url);
next(); next();
}); });
executeMiddlewareHandlers(app, '/scope/id', function(err) { executeMiddlewareHandlers(app, '/scope/id', function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['/id']); expect(steps).to.eql(['/id']);
done(); done();
}); });
}); });
@ -239,11 +274,13 @@ describe('app', function() {
app.middleware('initial', function(rq, rs, next) { app.middleware('initial', function(rq, rs, next) {
req = rq; req = rq;
res = rs; res = rs;
next(); next();
}); });
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([ expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([
'accepts', 'accepts',
'get', 'get',
@ -273,12 +310,15 @@ describe('app', function() {
var reqProps; var reqProps;
app.middleware('initial', function(req, res, next) { app.middleware('initial', function(req, res, next) {
reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl }; reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl };
next(); next();
}); });
executeMiddlewareHandlers(app, '/test/url', function(err) { executeMiddlewareHandlers(app, '/test/url', function(err) {
if (err) return done(err); if (err) return done(err);
expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' }); expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' });
done(); done();
}); });
}); });
@ -290,7 +330,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, '/test', function(err) { executeMiddlewareHandlers(app, '/test', function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['route', 'files']); expect(steps).to.eql(['route', 'files']);
done(); done();
}); });
}); });
@ -310,7 +352,9 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done; if (err) return done;
expect(steps).to.eql(numbers); expect(steps).to.eql(numbers);
done(); done();
}); });
}); });
@ -324,6 +368,7 @@ describe('app', function() {
mountpath: req.app.mountpath, mountpath: req.app.mountpath,
parent: req.app.parent parent: req.app.parent
}; };
next(); next();
}); });
subapp.on('mount', function() { mountWasEmitted = true; }); subapp.on('mount', function() { mountWasEmitted = true; });
@ -332,11 +377,13 @@ describe('app', function() {
executeMiddlewareHandlers(app, '/mountpath/test', function(err) { executeMiddlewareHandlers(app, '/mountpath/test', function(err) {
if (err) return done(err); if (err) return done(err);
expect(mountWasEmitted, 'mountWasEmitted').to.be.true; expect(mountWasEmitted, 'mountWasEmitted').to.be.true;
expect(data).to.eql({ expect(data).to.eql({
mountpath: '/mountpath', mountpath: '/mountpath',
parent: app parent: app
}); });
done(); done();
}); });
}); });
@ -350,25 +397,30 @@ describe('app', function() {
subapp.use(function verifyTestAssumptions(req, res, next) { subapp.use(function verifyTestAssumptions(req, res, next) {
expect(req.__proto__).to.not.equal(expected.req); expect(req.__proto__).to.not.equal(expected.req);
expect(res.__proto__).to.not.equal(expected.res); expect(res.__proto__).to.not.equal(expected.res);
next(); next();
}); });
app.middleware('initial', function saveOriginalValues(req, res, next) { app.middleware('initial', function saveOriginalValues(req, res, next) {
expected.req = req.__proto__; expected.req = req.__proto__;
expected.res = res.__proto__; expected.res = res.__proto__;
next(); next();
}); });
app.middleware('routes', subapp); app.middleware('routes', subapp);
app.middleware('final', function saveActualValues(req, res, next) { app.middleware('final', function saveActualValues(req, res, next) {
actual.req = req.__proto__; actual.req = req.__proto__;
actual.res = res.__proto__; actual.res = res.__proto__;
next(); next();
}); });
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(actual.req, 'req').to.equal(expected.req); expect(actual.req, 'req').to.equal(expected.req);
expect(actual.res, 'res').to.equal(expected.res); expect(actual.res, 'res').to.equal(expected.res);
done(); done();
}); });
}); });
@ -383,6 +435,7 @@ describe('app', function() {
function pathSavingHandler() { function pathSavingHandler() {
return function(req, res, next) { return function(req, res, next) {
steps.push(req.originalUrl); steps.push(req.originalUrl);
next(); next();
}; };
} }
@ -406,6 +459,7 @@ describe('app', function() {
var args = Array.prototype.slice.apply(arguments); var args = Array.prototype.slice.apply(arguments);
return function(req, res, next) { return function(req, res, next) {
steps.push(args); steps.push(args);
next(); next();
}; };
}; };
@ -456,12 +510,14 @@ describe('app', function() {
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql([ expect(steps).to.eql([
['before'], ['before'],
[expectedConfig], [expectedConfig],
['after', 2], ['after', 2],
[{x: 1}] [{x: 1}]
]); ]);
done(); done();
}); });
}); });
@ -472,6 +528,7 @@ describe('app', function() {
function factory() { function factory() {
return function(req, res, next) { return function(req, res, next) {
steps.push(req.originalUrl); steps.push(req.originalUrl);
next(); next();
}; };
}, },
@ -485,7 +542,9 @@ describe('app', function() {
function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(url, next) { executeMiddlewareHandlers(app, url, next); },
function(err) { function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(['/a', '/b', '/scope']); expect(steps).to.eql(['/a', '/b', '/scope']);
done(); done();
}); });
}); });
@ -542,24 +601,27 @@ describe('app', function() {
names.forEach(function(it) { names.forEach(function(it) {
app.middleware(it, function(req, res, next) { app.middleware(it, function(req, res, next) {
steps.push(it); steps.push(it);
next(); next();
}); });
}); });
executeMiddlewareHandlers(app, function(err) { executeMiddlewareHandlers(app, function(err) {
if (err) return done(err); if (err) return done(err);
expect(steps).to.eql(names); expect(steps).to.eql(names);
done(); done();
}); });
} }
}); });
describe('app.model(Model)', function() { describe('app.model(Model)', function() {
var app; var app, db, MyTestModel;
var db;
beforeEach(function() { beforeEach(function() {
app = loopback(); 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() { it('Expose a `Model` to remote clients', function() {
@ -570,8 +632,8 @@ describe('app', function() {
expect(app.models()).to.eql([Color]); expect(app.models()).to.eql([Color]);
}); });
it('uses singlar name as app.remoteObjects() key', function() { it('uses singular name as app.remoteObjects() key', function() {
var Color = PersistedModel.extend('color', {name: String}); var Color = PersistedModel.extend('color', { name: String });
app.model(Color); app.model(Color);
Color.attachTo(db); Color.attachTo(db);
expect(app.remoteObjects()).to.eql({ color: Color }); expect(app.remoteObjects()).to.eql({ color: Color });
@ -606,6 +668,22 @@ describe('app', function() {
expect(remotedClass).to.eql(Color.sharedClass); 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) { it.onServer('updates REST API when a new model is added', function(done) {
app.use(loopback.rest()); app.use(loopback.rest());
request(app).get('/colors').expect(404, function(err, res) { request(app).get('/colors').expect(404, function(err, res) {
@ -617,18 +695,22 @@ describe('app', function() {
}); });
}); });
it('accepts null dataSource', function() { it('accepts null dataSource', function(done) {
app.model('MyTestModel', { dataSource: null }); app.model(MyTestModel, { dataSource: null });
expect(MyTestModel.dataSource).to.eql(null);
done();
}); });
it('accepts false dataSource', function() { it('accepts false dataSource', function(done) {
app.model('MyTestModel', { dataSource: false }); app.model(MyTestModel, { dataSource: false });
expect(MyTestModel.getDataSource()).to.eql(null);
done();
}); });
it('should not require dataSource', function() { it('does not require dataSource', function(done) {
app.model('MyTestModel', {}); app.model(MyTestModel);
done();
}); });
}); });
describe('app.model(name, config)', function() { describe('app.model(name, config)', function() {
@ -636,6 +718,7 @@ describe('app', function() {
beforeEach(function() { beforeEach(function() {
app = loopback(); app = loopback();
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', { app.dataSource('db', {
connector: 'memory' connector: 'memory'
}); });
@ -689,7 +772,6 @@ describe('app', function() {
expect(app.models.foo.app).to.equal(app); expect(app.models.foo.app).to.equal(app);
expect(app.models.foo.shared).to.equal(true); expect(app.models.foo.shared).to.equal(true);
}); });
}); });
describe('app.model(ModelCtor, config)', function() { describe('app.model(ModelCtor, config)', function() {
@ -702,7 +784,8 @@ describe('app', function() {
} }
assert(!previousModel || !previousModel.dataSource); 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); expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db);
}); });
}); });
@ -710,7 +793,8 @@ describe('app', function() {
describe('app.models', function() { describe('app.models', function() {
it('is unique per app instance', function() { it('is unique per app instance', function() {
app.dataSource('db', { connector: 'memory' }); 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); expect(app.models.Color).to.equal(Color);
var anotherApp = loopback(); var anotherApp = loopback();
expect(anotherApp.models.Color).to.equal(undefined); expect(anotherApp.models.Color).to.equal(undefined);
@ -732,6 +816,22 @@ describe('app', function() {
app.dataSource('custom', { connector: 'custom' }); app.dataSource('custom', { connector: 'custom' });
expect(app.dataSources.custom.name).to.equal(loopback.Memory.name); 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() { describe.onServer('listen()', function() {
@ -755,6 +855,7 @@ describe('app', function() {
app.listen(function() { app.listen(function() {
expect(app.get('port'), 'port').to.not.equal(0); expect(app.get('port'), 'port').to.not.equal(0);
done(); done();
}); });
}); });
@ -768,6 +869,7 @@ describe('app', function() {
var host = process.platform === 'win32' ? 'localhost' : app.get('host'); var host = process.platform === 'win32' ? 'localhost' : app.get('host');
var expectedUrl = 'http://' + host + ':' + app.get('port') + '/'; var expectedUrl = 'http://' + host + ':' + app.get('port') + '/';
expect(app.get('url'), 'url').to.equal(expectedUrl); expect(app.get('url'), 'url').to.equal(expectedUrl);
done(); done();
}); });
}); });
@ -778,6 +880,7 @@ describe('app', function() {
app.listen(0, '127.0.0.1', function() { app.listen(0, '127.0.0.1', function() {
expect(app.get('port'), 'port').to.not.equal(0).and.not.equal(1); expect(app.get('port'), 'port').to.not.equal(0).and.not.equal(1);
expect(this.address().address).to.equal('127.0.0.1'); expect(this.address().address).to.equal('127.0.0.1');
done(); done();
}); });
}); });
@ -788,6 +891,7 @@ describe('app', function() {
app.set('port', 1); app.set('port', 1);
app.listen(0).on('listening', function() { app.listen(0).on('listening', function() {
expect(app.get('port'), 'port') .to.not.equal(0).and.not.equal(1); expect(app.get('port'), 'port') .to.not.equal(0).and.not.equal(1);
done(); done();
}); });
} }
@ -802,6 +906,7 @@ describe('app', function() {
app.listen() app.listen()
.on('listening', function() { .on('listening', function() {
expect(this.address().address).to.equal('127.0.0.1'); expect(this.address().address).to.equal('127.0.0.1');
done(); done();
}); });
}); });
@ -818,6 +923,7 @@ describe('app', function() {
var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping']; var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping'];
var app = loopback({ localRegistry: true, loadBuiltinModels: true }); var app = loopback({ localRegistry: true, loadBuiltinModels: true });
require('../lib/builtin-models')(app.registry); require('../lib/builtin-models')(app.registry);
app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' }); var db = app.dataSource('db', { connector: 'memory' });
app.enableAuth({ dataSource: 'db' }); app.enableAuth({ dataSource: 'db' });
@ -833,6 +939,7 @@ describe('app', function() {
it('detects already configured subclass of a required model', function() { it('detects already configured subclass of a required model', function() {
var app = loopback({ localRegistry: true, loadBuiltinModels: true }); var app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' }); var db = app.dataSource('db', { connector: 'memory' });
var Customer = app.registry.createModel('Customer', {}, { base: 'User' }); var Customer = app.registry.createModel('Customer', {}, { base: 'User' });
app.model(Customer, { dataSource: 'db' }); app.model(Customer, { dataSource: 'db' });
@ -853,18 +960,14 @@ describe('app', function() {
.end(function(err, res) { .end(function(err, res) {
if (err) return done(err); if (err) return done(err);
assert.equal(typeof res.body, 'object'); expect(res.body).to.be.an('object');
assert(res.body.started); expect(res.body).to.have.property('started');
// The number can be 0 expect(res.body.uptime, 'uptime').to.be.gte(0);
assert(res.body.uptime !== undefined);
var elapsed = Date.now() - Number(new Date(res.body.started)); var elapsed = Date.now() - Number(new Date(res.body.started));
// elapsed should be a positive number... // elapsed should be a small positive number...
assert(elapsed >= 0); expect(elapsed, 'elapsed').to.be.within(0, 300);
// less than 100 milliseconds
assert(elapsed < 100);
done(); done();
}); });
@ -957,8 +1060,18 @@ describe('app', function() {
}); });
function executeMiddlewareHandlers(app, urlPath, callback) { function executeMiddlewareHandlers(app, urlPath, callback) {
var handlerError;
var server = http.createServer(function(req, res) { 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') { if (callback === undefined && typeof urlPath === 'function') {
@ -969,6 +1082,6 @@ function executeMiddlewareHandlers(app, urlPath, callback) {
request(server) request(server)
.get(urlPath) .get(urlPath)
.end(function(err) { .end(function(err) {
if (err) return callback(err); callback(handlerError || err);
}); });
} }

View File

@ -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('PersistedModel.createChangeStream()', function() {
describe('configured to source changes locally', function() { describe('configured to source changes locally', function() {
before(function() { before(function() {
var test = this; var test = this;
var app = loopback({localRegistry: true}); var app = loopback({ localRegistry: true });
var ds = app.dataSource('ds', {connector: 'memory'}); var ds = app.dataSource('ds', { connector: 'memory' });
this.Score = app.model('Score', { var Score = app.registry.createModel('Score');
this.Score = app.model(Score, {
dataSource: 'ds', dataSource: 'ds',
changeDataSource: false // use only local observers changeDataSource: false // use only local observers
}); });
@ -17,6 +23,7 @@ describe('PersistedModel.createChangeStream()', function() {
changes.on('data', function(change) { changes.on('data', function(change) {
expect(change.type).to.equal('create'); expect(change.type).to.equal('create');
changes.destroy(); changes.destroy();
done(); done();
}); });
@ -31,6 +38,7 @@ describe('PersistedModel.createChangeStream()', function() {
changes.on('data', function(change) { changes.on('data', function(change) {
expect(change.type).to.equal('update'); expect(change.type).to.equal('update');
changes.destroy(); changes.destroy();
done(); done();
}); });
newScore.updateAttributes({ newScore.updateAttributes({
@ -47,6 +55,7 @@ describe('PersistedModel.createChangeStream()', function() {
changes.on('data', function(change) { changes.on('data', function(change) {
expect(change.type).to.equal('remove'); expect(change.type).to.equal('remove');
changes.destroy(); changes.destroy();
done(); done();
}); });

View File

@ -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 async = require('async');
var expect = require('chai').expect; var expect = require('chai').expect;
@ -28,9 +33,11 @@ describe('Change', function() {
}; };
TestModel.create(test.data, function(err, model) { TestModel.create(test.data, function(err, model) {
if (err) return done(err); if (err) return done(err);
test.model = model; test.model = model;
test.modelId = model.id; test.modelId = model.id;
test.revisionForModel = Change.revisionForInst(model); test.revisionForModel = Change.revisionForInst(model);
done(); done();
}); });
}); });
@ -61,6 +68,7 @@ describe('Change', function() {
var test = this; var test = this;
Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) { Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) {
if (err) return done(err); if (err) return done(err);
done(); done();
}); });
}); });
@ -69,6 +77,7 @@ describe('Change', function() {
var test = this; var test = this;
Change.find(function(err, trackedChanges) { Change.find(function(err, trackedChanges) {
assert.equal(trackedChanges[0].modelId, test.modelId.toString()); assert.equal(trackedChanges[0].modelId, test.modelId.toString());
done(); done();
}); });
}); });
@ -76,12 +85,47 @@ describe('Change', function() {
it('should only create one change', function(done) { it('should only create one change', function(done) {
Change.count(function(err, count) { Change.count(function(err, count) {
assert.equal(count, 1); assert.equal(count, 1);
done(); 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('Change.findOrCreateChange(modelName, modelId, callback)', function() {
describe('when a change doesnt exist', function() { describe('when a change doesnt exist', function() {
@ -89,7 +133,9 @@ describe('Change', function() {
var test = this; var test = this;
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) { Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
if (err) return done(err); if (err) return done(err);
test.result = result; test.result = result;
done(); done();
}); });
}); });
@ -98,7 +144,33 @@ describe('Change', function() {
var test = this; var test = this;
Change.findById(this.result.id, function(err, change) { Change.findById(this.result.id, function(err, change) {
if (err) return done(err); if (err) return done(err);
assert.equal(change.id, test.result.id); 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(); done();
}); });
}); });
@ -112,6 +184,7 @@ describe('Change', function() {
modelId: test.modelId modelId: test.modelId
}, function(err, change) { }, function(err, change) {
test.existingChange = change; test.existingChange = change;
done(); done();
}); });
}); });
@ -120,7 +193,9 @@ describe('Change', function() {
var test = this; var test = this;
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) { Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
if (err) return done(err); if (err) return done(err);
test.result = result; test.result = result;
done(); done();
}); });
}); });
@ -128,6 +203,7 @@ describe('Change', function() {
it('should find the entry', function(done) { it('should find the entry', function(done) {
var test = this; var test = this;
assert.equal(test.existingChange.id, test.result.id); assert.equal(test.existingChange.id, test.result.id);
done(); done();
}); });
}); });
@ -143,6 +219,7 @@ describe('Change', function() {
}, },
function(err, ch) { function(err, ch) {
change = ch; change = ch;
done(err); done(err);
}); });
}); });
@ -151,6 +228,7 @@ describe('Change', function() {
var test = this; var test = this;
change.rectify(function(err, ch) { change.rectify(function(err, ch) {
assert.equal(ch.rev, test.revisionForModel); assert.equal(ch.rev, test.revisionForModel);
done(); done();
}); });
}); });
@ -174,6 +252,7 @@ describe('Change', function() {
expect(change.type(), 'type').to.equal('update'); expect(change.type(), 'type').to.equal('update');
expect(change.prev, 'prev').to.equal(originalRev); expect(change.prev, 'prev').to.equal(originalRev);
expect(change.rev, 'rev').to.equal(test.revisionForModel); expect(change.rev, 'rev').to.equal(test.revisionForModel);
next(); next();
} }
], done); ], done);
@ -185,7 +264,9 @@ describe('Change', function() {
function checkpoint(next) { function checkpoint(next) {
TestModel.checkpoint(function(err, inst) { TestModel.checkpoint(function(err, inst) {
if (err) return next(err); if (err) return next(err);
cp = inst.seq; cp = inst.seq;
next(); next();
}); });
} }
@ -196,6 +277,7 @@ describe('Change', function() {
model.name += 'updated'; model.name += 'updated';
model.save(function(err) { model.save(function(err) {
test.revisionForModel = Change.revisionForInst(model); test.revisionForModel = Change.revisionForInst(model);
next(err); next(err);
}); });
} }
@ -211,14 +293,40 @@ describe('Change', function() {
change.rectify(function(err, c) { change.rectify(function(err, c) {
if (err) return done(err); if (err) return done(err);
expect(c.rev, 'rev').to.equal(originalRev); // sanity check expect(c.rev, 'rev').to.equal(originalRev); // sanity check
expect(c.checkpoint, 'checkpoint').to.equal(originalCheckpoint); expect(c.checkpoint, 'checkpoint').to.equal(originalCheckpoint);
done(); 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() { describe('change.currentRevision(callback)', function() {
it('should get the correct revision', function(done) { it('should get the correct revision', function(done) {
var test = this; var test = this;
@ -229,11 +337,30 @@ describe('Change', function() {
change.currentRevision(function(err, rev) { change.currentRevision(function(err, rev) {
assert.equal(rev, test.revisionForModel); assert.equal(rev, test.revisionForModel);
done(); 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() { describe('Change.hash(str)', function() {
// todo(ritch) test other hashing algorithms // todo(ritch) test other hashing algorithms
it('should hash the given string', function() { it('should hash the given string', function() {
@ -368,12 +495,34 @@ describe('Change', function() {
Change.diff(this.modelName, 0, remoteChanges, function(err, diff) { Change.diff(this.modelName, 0, remoteChanges, function(err, diff) {
if (err) return done(err); if (err) return done(err);
assert.equal(diff.deltas.length, 1); assert.equal(diff.deltas.length, 1);
assert.equal(diff.conflicts.length, 1); assert.equal(diff.conflicts.length, 1);
done(); 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) { it('should set "prev" to local revision in non-conflicting delta', function(done) {
var updateRecord = { var updateRecord = {
rev: 'foo-new', rev: 'foo-new',
@ -384,6 +533,7 @@ describe('Change', function() {
}; };
Change.diff(this.modelName, 0, [updateRecord], function(err, diff) { Change.diff(this.modelName, 0, [updateRecord], function(err, diff) {
if (err) return done(err); if (err) return done(err);
expect(diff.conflicts, 'conflicts').to.have.length(0); expect(diff.conflicts, 'conflicts').to.have.length(0);
expect(diff.deltas, 'deltas').to.have.length(1); expect(diff.deltas, 'deltas').to.have.length(1);
var actual = diff.deltas[0].toObject(); var actual = diff.deltas[0].toObject();
@ -395,6 +545,7 @@ describe('Change', function() {
prev: 'foo', // this is the current local revision prev: 'foo', // this is the current local revision
rev: 'foo-new', rev: 'foo-new',
}); });
done(); done();
}); });
}); });
@ -411,6 +562,7 @@ describe('Change', function() {
// with rev=foo CP=1 // with rev=foo CP=1
Change.diff(this.modelName, 2, [updateRecord], function(err, diff) { Change.diff(this.modelName, 2, [updateRecord], function(err, diff) {
if (err) return done(err); if (err) return done(err);
expect(diff.conflicts, 'conflicts').to.have.length(0); expect(diff.conflicts, 'conflicts').to.have.length(0);
expect(diff.deltas, 'deltas').to.have.length(1); expect(diff.deltas, 'deltas').to.have.length(1);
var actual = diff.deltas[0].toObject(); var actual = diff.deltas[0].toObject();
@ -422,6 +574,7 @@ describe('Change', function() {
prev: 'foo', // this is the current local revision prev: 'foo', // this is the current local revision
rev: 'foo-new', rev: 'foo-new',
}); });
done(); done();
}); });
}); });
@ -437,6 +590,7 @@ describe('Change', function() {
Change.diff(this.modelName, 0, [updateRecord], function(err, diff) { Change.diff(this.modelName, 0, [updateRecord], function(err, diff) {
if (err) return done(err); if (err) return done(err);
expect(diff.conflicts).to.have.length(0); expect(diff.conflicts).to.have.length(0);
expect(diff.deltas).to.have.length(1); expect(diff.deltas).to.have.length(1);
var actual = diff.deltas[0].toObject(); var actual = diff.deltas[0].toObject();
@ -448,6 +602,7 @@ describe('Change', function() {
prev: null, // this is the current local revision prev: null, // this is the current local revision
rev: 'new-rev', rev: 'new-rev',
}); });
done(); done();
}); });
}); });

View File

@ -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 async = require('async');
var loopback = require('../'); var loopback = require('../');
var expect = require('chai').expect;
// create a unique Checkpoint model
var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint'); var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint');
var memory = loopback.createDataSource({
connector: loopback.Memory
});
Checkpoint.attachTo(memory);
describe('Checkpoint', function() { 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) { it('returns the highest `seq` value', function(done) {
async.series([ async.series([
Checkpoint.create.bind(Checkpoint), Checkpoint.bumpLastSeq.bind(Checkpoint),
Checkpoint.create.bind(Checkpoint), Checkpoint.bumpLastSeq.bind(Checkpoint),
function(next) { function(next) {
Checkpoint.current(function(err, seq) { Checkpoint.current(function(err, seq) {
if (err) next(err); if (err) next(err);
expect(seq).to.equal(3); expect(seq).to.equal(3);
next(); next();
}); });
} }
], done); ], 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);
});
});
}); });
}); });

View File

@ -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');
}
});

View File

@ -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() { describe('DataSource', function() {
var memory; var memory;
@ -17,6 +22,7 @@ describe('DataSource', function() {
assert.isFunc(Color, 'findOne'); assert.isFunc(Color, 'findOne');
assert.isFunc(Color, 'create'); assert.isFunc(Color, 'create');
assert.isFunc(Color, 'updateOrCreate'); assert.isFunc(Color, 'updateOrCreate');
assert.isFunc(Color, 'upsertWithWhere');
assert.isFunc(Color, 'upsert'); assert.isFunc(Color, 'upsert');
assert.isFunc(Color, 'findOrCreate'); assert.isFunc(Color, 'findOrCreate');
assert.isFunc(Color, 'exists'); assert.isFunc(Color, 'exists');
@ -78,6 +84,7 @@ describe('DataSource', function() {
existsAndShared('_forDB', false); existsAndShared('_forDB', false);
existsAndShared('create', true); existsAndShared('create', true);
existsAndShared('updateOrCreate', true); existsAndShared('updateOrCreate', true);
existsAndShared('upsertWithWhere', true);
existsAndShared('upsert', true); existsAndShared('upsert', true);
existsAndShared('findOrCreate', false); existsAndShared('findOrCreate', false);
existsAndShared('exists', true); existsAndShared('exists', true);

View File

@ -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 path = require('path');
var loopback = require('../../'); var loopback = require('../../');
var models = require('../fixtures/e2e/models'); var models = require('../fixtures/e2e/models');
@ -19,7 +24,9 @@ describe('RemoteConnector', function() {
foo: 'bar' foo: 'bar'
}, function(err, inst) { }, function(err, inst) {
if (err) return done(err); if (err) return done(err);
assert(inst.id); assert(inst.id);
done(); done();
}); });
}); });
@ -30,7 +37,9 @@ describe('RemoteConnector', function() {
}); });
m.save(function(err, data) { m.save(function(err, data) {
if (err) return done(err); if (err) return done(err);
assert(data.foo === 'bar'); assert(data.foo === 'bar');
done(); done();
}); });
}); });

View File

@ -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 path = require('path');
var loopback = require('../../'); var loopback = require('../../');
var models = require('../fixtures/e2e/models'); var models = require('../fixtures/e2e/models');
@ -27,8 +32,10 @@ describe('Replication', function() {
}, function(err, created) { }, function(err, created) {
LocalTestModel.replicate(0, TestModel, function() { LocalTestModel.replicate(0, TestModel, function() {
if (err) return done(err); 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); assert.equal(created.id, found.id);
done(); done();
}); });
}); });

View File

@ -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 loopback = require('../');
var MyEmail; var MyEmail;
var assert = require('assert'); var assert = require('assert');
@ -33,6 +38,14 @@ describe('Email connector', function() {
assert(connector.transportForName('smtp')); 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() { describe('Email and SMTP', function() {
@ -61,6 +74,7 @@ describe('Email and SMTP', function() {
assert(mail.response); assert(mail.response);
assert(mail.envelope); assert(mail.envelope);
assert(mail.messageId); assert(mail.messageId);
done(err); done(err);
}); });
}); });
@ -78,6 +92,7 @@ describe('Email and SMTP', function() {
assert(mail.response); assert(mail.response);
assert(mail.envelope); assert(mail.envelope);
assert(mail.messageId); assert(mail.messageId);
done(err); done(err);
}); });
}); });

View File

@ -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 loopback = require('../');
var app; var app;
var assert = require('assert'); var expect = require('chai').expect;
var request = require('supertest'); var request = require('supertest');
describe('loopback.errorHandler(options)', function() { describe('loopback.errorHandler(options)', function() {
@ -16,7 +21,9 @@ describe('loopback.errorHandler(options)', function() {
request(app) request(app)
.get('/url-does-not-exist') .get('/url-does-not-exist')
.end(function(err, res) { .end(function(err, res) {
assert.ok(res.error.text.match(/<ul id="stacktrace"><li> &nbsp; &nbsp;at raiseUrlNotFoundError/)); expect(res.error.text).to.match(
/<ul id="stacktrace"><li>( &nbsp;)+at raiseUrlNotFoundError/);
done(); done();
}); });
}); });
@ -32,7 +39,8 @@ describe('loopback.errorHandler(options)', function() {
request(app) request(app)
.get('/url-does-not-exist') .get('/url-does-not-exist')
.end(function(err, res) { .end(function(err, res) {
assert.ok(res.error.text.match(/<ul id="stacktrace"><\/ul>/)); expect(res.error.text).to.match(/<ul id="stacktrace"><\/ul>/);
done(); done();
}); });
}); });
@ -41,15 +49,24 @@ describe('loopback.errorHandler(options)', function() {
//arrange //arrange
var app = loopback(); var app = loopback();
app.use(loopback.urlNotFound()); app.use(loopback.urlNotFound());
app.use(loopback.errorHandler({ includeStack: false, log: customLogger }));
var errorLogged;
app.use(loopback.errorHandler({
includeStack: false,
log: function customLogger(err, str, req) {
errorLogged = err;
}
}));
//act //act
request(app).get('/url-does-not-exist').end(); request(app).get('/url-does-not-exist').end(function(err) {
if (err) return done(err);
//assert
expect(errorLogged)
.to.have.property('message', 'Cannot GET /url-does-not-exist');
//assert
function customLogger(err, str, req) {
assert.ok(err.message === 'Cannot GET /url-does-not-exist');
done(); done();
} });
}); });
}); });

View File

@ -1,5 +1,6 @@
{ {
"name": "account", "name": "accountWithReplaceOnPUTtrue",
"plural": "accounts-replacing",
"relations": { "relations": {
"transactions": { "transactions": {
"model": "transaction", "model": "transaction",
@ -38,5 +39,6 @@
"principalId": "$dummy" "principalId": "$dummy"
} }
], ],
"properties": {} "properties": {},
"replaceOnPUT": true
} }

View File

@ -0,0 +1,44 @@
{
"name": "accountWithReplaceOnPUTfalse",
"plural": "accounts-updating",
"relations": {
"transactions": {
"model": "transaction",
"type": "hasMany"
},
"user": {
"model": "user",
"type": "belongsTo",
"foreignKey": "userId"
}
},
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"accessType": "*",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$owner"
},
{
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$owner",
"property": "deleteById"
},
{
"accessType": "*",
"permission": "DENY",
"property": "find",
"principalType": "ROLE",
"principalId": "$dummy"
}
],
"properties": {},
"replaceOnPUT": false
}

View File

@ -22,6 +22,12 @@
"permission": "ALLOW", "permission": "ALLOW",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone" "principalId": "$everyone"
},
{
"accessType": "WRITE",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$dynamic-role"
} }
], ],
"properties": {} "properties": {}

View File

@ -19,5 +19,6 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone" "principalId": "$everyone"
} }
] ],
"replaceOnPUT": false
} }

View File

@ -1,5 +1,6 @@
{ {
"port": 3000, "port": 3000,
"host": "0.0.0.0", "host": "0.0.0.0",
"logoutSessionsOnSensitiveChanges": true,
"legacyExplorer": false "legacyExplorer": false
} }

View File

@ -2,7 +2,8 @@
"_meta": { "_meta": {
"sources": [ "sources": [
"../common/models", "../common/models",
"./models" "./models",
"../../../../common/models"
] ]
}, },
"ACL": { "ACL": {
@ -33,7 +34,11 @@
"public": true, "public": true,
"dataSource": "db" "dataSource": "db"
}, },
"account": { "accountWithReplaceOnPUTtrue": {
"public": true,
"dataSource": "db"
},
"accountWithReplaceOnPUTfalse": {
"public": true, "public": true,
"dataSource": "db" "dataSource": "db"
}, },

View File

@ -1,6 +1,14 @@
// 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 loopback = require('../../../..');
var boot = require('loopback-boot'); var boot = require('loopback-boot');
var app = module.exports = loopback(); var app = module.exports = loopback({
localRegistry: true,
loadBuiltinModels: true
});
boot(app, __dirname); boot(app, __dirname);

View File

@ -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 loopback = require('../../../../index'); var loopback = require('../../../../index');
var PersistedModel = loopback.PersistedModel; var PersistedModel = loopback.PersistedModel;

View File

@ -1,5 +1,10 @@
// 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('../../../../index'); var loopback = require('../../../../index');
var app = module.exports = loopback(); var app = module.exports = loopback({ localRegistry: true });
var models = require('./models'); var models = require('./models');
var TestModel = models.TestModel; var TestModel = models.TestModel;

View File

@ -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
module.exports = function(Todo) { module.exports = function(Todo) {
}; };

View File

@ -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 boot = require('loopback-boot'); var boot = require('loopback-boot');
var loopback = require('../../../../../index'); var loopback = require('../../../../../index');

Some files were not shown because too many files have changed in this diff Show More