Add end-to-end unit-tests verifying enforcement of access control during
conflict resolution.
Implement two facade methods providing REST API for Change methods used
by conflict resolution:
PersistedModel.findLastChange
GET /api/{model.pluralName}/{id}/changes/last
PersistedModel.updateLastChange
PUT /api/{model.pluralName}/{id}/changes/last
By providing these two methods on PersistedModel, replication users
don't have to expose the Change model via the REST API. What's even
more important, these two methods use the same set of ACL rules
as other (regular) PersistedModel methods.
Rework `Conflict.prototype.changes()` and `Conflict.prototype.resolve()`
to use these new facade methods.
Implement a new method `Conflict.prototype.swapParties()` that provides
better API for the situation when a conflict detected in Remote->Local
replication should be resolved locally (i.e. in the replication target).
Correctly handle the case when the model is attached multiple times
during the lifecycle, this happens because `loopback.createModel`
always makes an attempt to auto-attach.
1) Add integration tests running change replication over REST to verify
that access control at model level is correctly enforced.
2) Implement a new access type "REPLICATE" that allows principals
to create new checkpoints, even though they don't have full WRITE
access to the model. Together with the "READ" permission, these
two types allow principals to replicate (pull) changes from the server.
Note that anybody having "WRITE" access type is automatically
granted "REPLICATE" type too.
3) Add a new model option "enableRemoteReplication" that exposes
replication methods via strong remoting, but does not configure
change rectification. This option should be used the clients
when setting up Remote models attached to the server via the remoting
connector.
- `loopback.registry` is now a true global registry
- `app.registry` is unique per app object
- `Model.registry` is set when a Model is created using any registry method
- `loopback.localRegistry` and `loopback({localRegistry: true})` when set to `true` this will create a `Registry` per `Application`. It defaults to `false`.
Modify the files to export a model factory function accepting
a `registry` argument. This is a preparation step for per-application
models - see #1212.
Deprecate `Change.handleError`, it was used inconsistenly for a subset
of possible errors only. Rework all `Change` methods to always report
all errors to the caller via the callback.
Rework `PersistedModel` to report change-tracking errors via the
existing method `PersistedModel.handleChangeError`. This method
can be customized on a per-model basis to provide different error
handling.
The default implementation emits `error` event on the model class,
users can attach an event listener that can provide a custom error
handler.
NOTE: Unhandled `error` events crash the application by default.
Refactor the core implementation of current context from
server/middleware/context.js into server/current-context.js.
Expose new public API:
- loopback.runInContext
- loopback.createContext
Use the recently added context property `ctx.instance` to improve
the accuracy of the algorithm detecting whether a single or
multiple models were deleted.
Modify `Change.diff()` to include current data revision in each
delta reported back. The current data revision is stored in
`delta.prev`.
Modify `PersistedModel.bulkUpdate()` to check that the current data
revision matches `delta.prev` and report a conflict if a third party
has modified the database under our hands.
Fix `Change` implementation and tests so that they are no longer
attempting to create instances with duplicate ids.
(This used to work because the memory connector was silently
converting such requests to updateOrCreate/findOrCreate.)
Rework the Change model to merge changes made within the same
Checkpoint.
Rework `replicate()` to run multiple iteration until there were no
changes replicated. This ensures that the target model is left in
a clean state with no pending changes associated with the latest
(current) checkpoint.
Extend `PersistedModel.replicate` to pass the newly created checkpoints
as the third callback argument.
The typical usage of these values is to pass them as the `since`
argument of the next `replicate()` call.
global.since = -1;
function sync(cb) {
LocalModel.replicate(
since,
RemoteModel,
function(err, conflicts, cps)
if (err) return cb(err);
if (!conflicts.length) {
since = cps;
return cb();
}
// resolve conflicts and try again
});
}
Before this change, in the case of a one-way replication, the remote
checkpoint was never updated, thus the "diff" step had to check
all changes made through the whole history.
This commit fixes the problem by creating a new remote checkpoint
at the same time when a local checkpoint is created.
It is important to create the new checkpoint before the replication is
started to prevent a race condition where a remote change can end up
being associated with an already replicated checkpoint.
Rework the "replicate()" to create a new source checkpoint as the first
step, so that any changes made in parallel will be associated with
the next checkpoint.
Before this commit, there was a race condition where a change might
end up being associated with the already-replicated checkpoint and thus
not picked up by the next replication run.
Modify `PersistedModel.replicate` to allow consumers to provide
different "since" values to be used for the local and remote changes.
Make the "since" filters consistent and include the "since" value
in the range via `gte`. Before this commit, the local query used
`gt` and the remote query used `gte`.
Add unit-tests to verify that all DAO methods correctly create change
records.
Rework the change detection to use the new operation hooks, this fixes
the bugs where operations like "updateOrCreate" did not update change
records.
Express has recently deprecated `req.param()` to force developers
to be explicit about the source of the value. To avoid deprecation
warnings, this commit replaces all calls of `req.param()` with a
simplified inline version.
Enhance the error objects with a `code` property containing
a machine-readable string code describing the error, for example
INVALID_TOKEN or USER_NOT_FOUND.
Also improve 404 error messages to include the model name.
Bugs fixed:
- express helpers like `req.get` are now available in middleware
handlers registered via `app.middleware`
- `req.url` does not include the mountpath prefix now, this is
consistent with the behaviour of `app.use`
The implementation of phased middleware was completely rewritten.
- We no longer use Phase and PhaseList objects from loopback-phase.
- Handler functions are registered via the `Layer` mechanism used by
express router.
- The app keeps the layers sorted according to phases.
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
The new location allows developer to use the following identifiers
when loading the middleware using the new declarative style:
app.middlewareFromConfig(
require('loopback/server/middleware/rest'),
{ phase: 'routes' });
app.middlewareFromConfig(
require('loopback/server/middleware/url-not-found'),
{ phase: 'final' });
Refactor the implementation to use the new method `phaseList.zipMerge`.
This is commit is changing the behaviour in the case when
the first new phase does not exist in the current list.
Before the change, all new phases were added just before the "routes"
phase.
After this change, new phases are added to the head of the list,
until an existing phase is encountered, at which point the regular
merge algorithm kicks in.
Example:
app.defineMiddlewarePhases(['first', 'routes', 'subapps']);
Before the change: code throws an error - 'routes' already exists.
After the change: phases are merged with the following result:
'first', 'initial', ..., 'routes', 'subapps', ...
Implement method for registering (new) middleware phases.
- If all names are new, then the phases are added just before
the "routes" phase.
- Otherwise the provided list of names is merged with the existing
phases in such way that the order of phases is preserved.
Example
// built-in phases:
// initial, session, auth, parse, routes, files, final
app.defineMiddlewarePhases('custom');
// new list of phases
// initial, session, auth, parse,
// custom,
// routes, files, final
app.defineMiddlewarePhases([
'initial', 'postinit', 'preauth', 'routes', 'subapps'
]);
// new list of phases
// initial,
// postinit, preauth,
// session, auth, parse, custom,
// routes,
// subapps,
// files, final
Implement a function registering a middleware using a factory function
and a JSON config.
Example:
app.middlewareFromConfig(compression, {
enabled: true,
phase: 'initial',
config: {
threshold: 128
}
});
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
Modify `loopback.rest()` to read the configuration for
`loopback.context` from `app.get('remoting')`, which is the approach
used for all other configuration options related to the REST transport.
- Implement the middleware `loopback.context`
- Inject context into juggler and strong-remoting
- Make http context optional and default to false
- Optionally mount context middleware from `loopback.rest`
Allow the developer to pass custom `remoting` options via Model
settings, e.g.
PersistedModel.extend(
'MyModel',
{ name: String },
{
remoting: { normalizeHttpPath: true }
});
Also add `options` arg to `app.handler`, this object is passed directly
to strong-remoting handler.
When running on Unix and no hostname is specified, use `0.0.0.0`
as the hostname instead of `localhost`.
When running on Windows and the hostname is either not specified or
it is `0.0.0.0` or `::`, use `localhost` in the URL. The reason is
that Windows cannot open URLs using `0.0.0.0` as a hostname.