Merge branch 'master' into feature/fix-issue-377
This commit is contained in:
commit
335bae4b46
|
@ -11,3 +11,5 @@
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
|
*xunit.xml
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Breaking Changes
|
||||||
|
|
||||||
|
# 1.9
|
||||||
|
|
||||||
|
## Remote Method API
|
||||||
|
|
||||||
|
`loopback.remoteMethod()` is now deprecated.
|
||||||
|
|
||||||
|
Defining remote methods now should be done like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// static
|
||||||
|
MyModel.greet = function(msg, cb) {
|
||||||
|
cb(null, 'greetings... ' + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
MyModel.remoteMethod(
|
||||||
|
'greet',
|
||||||
|
{
|
||||||
|
accepts: [{arg: 'msg', type: 'string'}],
|
||||||
|
returns: {arg: 'greeting', type: 'string'}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE: remote instance method support is also now deprecated...
|
||||||
|
Use static methods instead. If you absolutely need it you can still set
|
||||||
|
`options.isStatic = false`** We plan to drop support for instance methods in
|
||||||
|
`2.0`.
|
||||||
|
|
||||||
|
## Remote Instance Methods
|
||||||
|
|
||||||
|
All remote instance methods have been replaced with static replacements.
|
||||||
|
|
||||||
|
The REST API is backwards compatible.
|
118
Gruntfile.js
118
Gruntfile.js
|
@ -1,6 +1,8 @@
|
||||||
/*global module:false*/
|
/*global module:false*/
|
||||||
module.exports = function(grunt) {
|
module.exports = function(grunt) {
|
||||||
|
|
||||||
|
grunt.loadNpmTasks('grunt-mocha-test');
|
||||||
|
|
||||||
// Project configuration.
|
// Project configuration.
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
// Metadata.
|
// Metadata.
|
||||||
|
@ -53,86 +55,39 @@ module.exports = function(grunt) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
karma: {
|
mochaTest: {
|
||||||
unit: {
|
'unit': {
|
||||||
|
src: 'test/*.js',
|
||||||
options: {
|
options: {
|
||||||
// base path, that will be used to resolve files and exclude
|
reporter: 'dot',
|
||||||
basePath: '',
|
|
||||||
|
|
||||||
// frameworks to use
|
|
||||||
frameworks: ['mocha', 'browserify'],
|
|
||||||
|
|
||||||
// list of files / patterns to load in the browser
|
|
||||||
files: [
|
|
||||||
'test/support.js',
|
|
||||||
'test/model.test.js',
|
|
||||||
'test/geo-point.test.js'
|
|
||||||
],
|
|
||||||
|
|
||||||
// list of files to exclude
|
|
||||||
exclude: [
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
// test results reporter to use
|
|
||||||
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
|
|
||||||
reporters: ['dots'],
|
|
||||||
|
|
||||||
// web server port
|
|
||||||
port: 9876,
|
|
||||||
|
|
||||||
// cli runner port
|
|
||||||
runnerPort: 9100,
|
|
||||||
|
|
||||||
// enable / disable colors in the output (reporters and logs)
|
|
||||||
colors: true,
|
|
||||||
|
|
||||||
// level of logging
|
|
||||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
|
||||||
logLevel: 'warn',
|
|
||||||
|
|
||||||
// enable / disable watching file and executing tests whenever any file changes
|
|
||||||
autoWatch: true,
|
|
||||||
|
|
||||||
// Start these browsers, currently available:
|
|
||||||
// - Chrome
|
|
||||||
// - ChromeCanary
|
|
||||||
// - Firefox
|
|
||||||
// - Opera
|
|
||||||
// - Safari (only Mac)
|
|
||||||
// - PhantomJS
|
|
||||||
// - IE (only Windows)
|
|
||||||
browsers: [
|
|
||||||
'Chrome'
|
|
||||||
],
|
|
||||||
|
|
||||||
// If browser does not capture in given timeout [ms], kill it
|
|
||||||
captureTimeout: 60000,
|
|
||||||
|
|
||||||
// Continuous Integration mode
|
|
||||||
// if true, it capture browsers, run tests and exit
|
|
||||||
singleRun: false,
|
|
||||||
|
|
||||||
// Browserify config (all optional)
|
|
||||||
browserify: {
|
|
||||||
// extensions: ['.coffee'],
|
|
||||||
ignore: [
|
|
||||||
'nodemailer',
|
|
||||||
'passport',
|
|
||||||
'passport-local',
|
|
||||||
'superagent',
|
|
||||||
'supertest'
|
|
||||||
],
|
|
||||||
// transform: ['coffeeify'],
|
|
||||||
// debug: true,
|
|
||||||
// noParse: ['jquery'],
|
|
||||||
watch: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Add browserify to preprocessors
|
|
||||||
preprocessors: {'test/*': ['browserify']}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'unit-xml': {
|
||||||
|
src: 'test/*.js',
|
||||||
|
options: {
|
||||||
|
reporter: 'xunit',
|
||||||
|
captureFile: 'xunit.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
karma: {
|
||||||
|
'unit-once': {
|
||||||
|
configFile: 'test/karma.conf.js',
|
||||||
|
browsers: [ 'PhantomJS' ],
|
||||||
|
singleRun: true,
|
||||||
|
reporters: ['dots', 'junit'],
|
||||||
|
|
||||||
|
// increase the timeout for slow build slaves (e.g. Travis-ci)
|
||||||
|
browserNoActivityTimeout: 30000,
|
||||||
|
|
||||||
|
// CI friendly test output
|
||||||
|
junitReporter: {
|
||||||
|
outputFile: 'karma-xunit.xml'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unit: {
|
||||||
|
configFile: 'test/karma.conf.js',
|
||||||
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
options: {
|
options: {
|
||||||
// base path, that will be used to resolve files and exclude
|
// base path, that will be used to resolve files and exclude
|
||||||
|
@ -143,7 +98,8 @@ module.exports = function(grunt) {
|
||||||
|
|
||||||
// list of files / patterns to load in the browser
|
// list of files / patterns to load in the browser
|
||||||
files: [
|
files: [
|
||||||
'test/e2e/remote-connector.e2e.js'
|
'test/e2e/remote-connector.e2e.js',
|
||||||
|
'test/e2e/replication.e2e.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
// list of files to exclude
|
// list of files to exclude
|
||||||
|
@ -232,4 +188,10 @@ module.exports = function(grunt) {
|
||||||
// Default task.
|
// Default task.
|
||||||
grunt.registerTask('default', ['browserify']);
|
grunt.registerTask('default', ['browserify']);
|
||||||
|
|
||||||
|
grunt.registerTask('test', [
|
||||||
|
process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit',
|
||||||
|
'karma:unit-once']);
|
||||||
|
|
||||||
|
// alias for sl-ci-run and `npm test`
|
||||||
|
grunt.registerTask('mocha-and-karma', ['test']);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"lib/loopback.js",
|
"lib/loopback.js",
|
||||||
"lib/runtime.js",
|
"lib/runtime.js",
|
||||||
"lib/registry.js",
|
"lib/registry.js",
|
||||||
{ "title": "Base model", "depth": 2 },
|
{ "title": "Base models", "depth": 2 },
|
||||||
"lib/models/model.js",
|
"lib/models/model.js",
|
||||||
"lib/models/data-model.js",
|
"lib/models/persisted-model.js",
|
||||||
{ "title": "Middleware", "depth": 2 },
|
{ "title": "Middleware", "depth": 2 },
|
||||||
"lib/middleware/rest.js",
|
"lib/middleware/rest.js",
|
||||||
"lib/middleware/status.js",
|
"lib/middleware/status.js",
|
||||||
|
@ -19,7 +19,8 @@
|
||||||
"lib/models/application.js",
|
"lib/models/application.js",
|
||||||
"lib/models/email.js",
|
"lib/models/email.js",
|
||||||
"lib/models/role.js",
|
"lib/models/role.js",
|
||||||
"lib/models/user.js"
|
"lib/models/user.js",
|
||||||
|
"lib/models/change.js"
|
||||||
],
|
],
|
||||||
"assets": "/docs/assets"
|
"assets": "/docs/assets"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
var loopback = require('../../');
|
var loopback = require('../../');
|
||||||
|
|
||||||
var CartItem = exports.CartItem = loopback.DataModel.extend('CartItem', {
|
var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', {
|
||||||
tax: {type: Number, default: 0.1},
|
tax: {type: Number, default: 0.1},
|
||||||
price: Number,
|
price: Number,
|
||||||
item: String,
|
item: String,
|
||||||
|
@ -22,8 +22,7 @@ CartItem.sum = function(cartId, callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loopback.remoteMethod(
|
CartItem.remoteMethod('sum',
|
||||||
CartItem.sum,
|
|
||||||
{
|
{
|
||||||
accepts: {arg: 'cartId', type: 'number'},
|
accepts: {arg: 'cartId', type: 'number'},
|
||||||
returns: {arg: 'total', type: 'number'}
|
returns: {arg: 'total', type: 'number'}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
var loopback = require('../../');
|
||||||
|
var app = loopback();
|
||||||
|
var db = app.dataSource('db', {connector: loopback.Memory});
|
||||||
|
var Color = app.model('color', {dataSource: 'db', options: {trackChanges: true}});
|
||||||
|
var Color2 = app.model('color2', {dataSource: 'db', options: {trackChanges: true}});
|
||||||
|
var target = Color2;
|
||||||
|
var source = Color;
|
||||||
|
var SPEED = process.env.SPEED || 100;
|
||||||
|
var conflicts;
|
||||||
|
|
||||||
|
var steps = [
|
||||||
|
|
||||||
|
createSomeInitialSourceData,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data'),
|
||||||
|
list.bind(this, target, 'current TARGET data'),
|
||||||
|
|
||||||
|
updateSomeTargetData,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data '),
|
||||||
|
list.bind(this, target, 'current TARGET data (includes conflicting update)'),
|
||||||
|
|
||||||
|
updateSomeSourceDataCausingAConflict,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data (now has a conflict)'),
|
||||||
|
list.bind(this, target, 'current TARGET data (includes conflicting update)'),
|
||||||
|
|
||||||
|
resolveAllConflicts,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data (conflict resolved)'),
|
||||||
|
list.bind(this, target, 'current TARGET data (conflict resolved)'),
|
||||||
|
|
||||||
|
createMoreSourceData,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data'),
|
||||||
|
list.bind(this, target, 'current TARGET data'),
|
||||||
|
|
||||||
|
createEvenMoreSourceData,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data'),
|
||||||
|
list.bind(this, target, 'current TARGET data'),
|
||||||
|
|
||||||
|
deleteAllSourceData,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data (empty)'),
|
||||||
|
list.bind(this, target, 'current TARGET data (empty)'),
|
||||||
|
|
||||||
|
createSomeNewSourceData,
|
||||||
|
|
||||||
|
replicateSourceToTarget,
|
||||||
|
list.bind(this, source, 'current SOURCE data'),
|
||||||
|
list.bind(this, target, 'current TARGET data')
|
||||||
|
];
|
||||||
|
|
||||||
|
run(steps);
|
||||||
|
|
||||||
|
function createSomeInitialSourceData() {
|
||||||
|
Color.create([
|
||||||
|
{name: 'red'},
|
||||||
|
{name: 'blue'},
|
||||||
|
{name: 'green'}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replicateSourceToTarget() {
|
||||||
|
Color.replicate(0, Color2, {}, function(err, replicationConflicts) {
|
||||||
|
conflicts = replicationConflicts;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAllConflicts() {
|
||||||
|
if(conflicts.length) {
|
||||||
|
conflicts.forEach(function(conflict) {
|
||||||
|
conflict.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSomeTargetData() {
|
||||||
|
Color2.findById(1, function(err, color) {
|
||||||
|
color.name = 'conflict';
|
||||||
|
color.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMoreSourceData() {
|
||||||
|
Color.create({name: 'orange'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEvenMoreSourceData() {
|
||||||
|
Color.create({name: 'black'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSomeSourceDataCausingAConflict() {
|
||||||
|
Color.findById(1, function(err, color) {
|
||||||
|
color.name = 'red!!!!';
|
||||||
|
color.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAllSourceData() {
|
||||||
|
Color.destroyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSomeNewSourceData() {
|
||||||
|
Color.create([
|
||||||
|
{name: 'violet'},
|
||||||
|
{name: 'amber'},
|
||||||
|
{name: 'olive'}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function list(model, msg) {
|
||||||
|
console.log(msg);
|
||||||
|
model.find(function(err, items) {
|
||||||
|
items.forEach(function(item) {
|
||||||
|
console.log(' -', item.name);
|
||||||
|
});
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(steps) {
|
||||||
|
setInterval(function() {
|
||||||
|
var step = steps.shift();
|
||||||
|
if(step) {
|
||||||
|
console.log(step.name);
|
||||||
|
step();
|
||||||
|
}
|
||||||
|
}, SPEED);
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 894 B |
|
@ -4,13 +4,11 @@
|
||||||
|
|
||||||
var DataSource = require('loopback-datasource-juggler').DataSource
|
var DataSource = require('loopback-datasource-juggler').DataSource
|
||||||
, registry = require('./registry')
|
, registry = require('./registry')
|
||||||
, compat = require('./compat')
|
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, fs = require('fs')
|
, fs = require('fs')
|
||||||
, extend = require('util')._extend
|
, extend = require('util')._extend
|
||||||
, _ = require('underscore')
|
, _ = require('underscore')
|
||||||
, RemoteObjects = require('strong-remoting')
|
, RemoteObjects = require('strong-remoting')
|
||||||
, swagger = require('strong-remoting/ext/swagger')
|
|
||||||
, stringUtils = require('underscore.string')
|
, stringUtils = require('underscore.string')
|
||||||
, path = require('path');
|
, path = require('path');
|
||||||
|
|
||||||
|
@ -94,7 +92,7 @@ app.disuse = function (route) {
|
||||||
* var User = loopback.User;
|
* var User = loopback.User;
|
||||||
* app.model(User, { dataSource: 'db' });
|
* app.model(User, { dataSource: 'db' });
|
||||||
*
|
*
|
||||||
* // The old way: create and attach a new model (deprecated)
|
* // LoopBack 1.x way: create and attach a new model (deprecated)
|
||||||
* var Widget = app.model('Widget', {
|
* var Widget = app.model('Widget', {
|
||||||
* dataSource: 'db',
|
* dataSource: 'db',
|
||||||
* properties: {
|
* properties: {
|
||||||
|
@ -152,13 +150,14 @@ app.model = function (Model, config) {
|
||||||
|
|
||||||
this.models().push(Model);
|
this.models().push(Model);
|
||||||
|
|
||||||
if (isPublic) {
|
if (isPublic && Model.sharedClass) {
|
||||||
var remotingClassName = compat.getClassNameForRemoting(Model);
|
this.remotes().addClass(Model.sharedClass);
|
||||||
this.remotes().exports[remotingClassName] = Model;
|
if (Model.settings.trackChanges && Model.Change)
|
||||||
|
this.remotes().addClass(Model.Change.sharedClass);
|
||||||
clearHandlerCache(this);
|
clearHandlerCache(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Model.shared = isPublic; // The base Model has shared = true
|
Model.shared = isPublic;
|
||||||
Model.app = this;
|
Model.app = this;
|
||||||
Model.emit('attached', this);
|
Model.emit('attached', this);
|
||||||
return Model;
|
return Model;
|
||||||
|
@ -167,9 +166,6 @@ app.model = function (Model, config) {
|
||||||
/**
|
/**
|
||||||
* Get the models exported by the app. Returns only models defined using `app.model()`
|
* Get the models exported by the app. Returns only models defined using `app.model()`
|
||||||
*
|
*
|
||||||
* **Deprecated. Use the package
|
|
||||||
* [loopback-boot](https://github.com/strongloop/loopback-boot) instead.**
|
|
||||||
|
|
||||||
* There are two ways to access models:
|
* There are two ways to access models:
|
||||||
*
|
*
|
||||||
* 1. Call `app.models()` to get a list of all models.
|
* 1. Call `app.models()` to get a list of all models.
|
||||||
|
@ -261,47 +257,14 @@ app.connector = function(name, connector) {
|
||||||
|
|
||||||
app.remoteObjects = function () {
|
app.remoteObjects = function () {
|
||||||
var result = {};
|
var result = {};
|
||||||
var models = this.models();
|
|
||||||
|
this.remotes().classes().forEach(function(sharedClass) {
|
||||||
// add in models
|
result[sharedClass.name] = sharedClass.ctor;
|
||||||
models.forEach(function (ModelCtor) {
|
|
||||||
// only add shared models
|
|
||||||
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
|
|
||||||
result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable swagger REST API documentation.
|
|
||||||
*
|
|
||||||
* **Note**: This method is deprecated. Use [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
|
|
||||||
*
|
|
||||||
* **Options**
|
|
||||||
*
|
|
||||||
* - `basePath` The basepath for your API - eg. 'http://localhost:3000'.
|
|
||||||
*
|
|
||||||
* **Example**
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* // enable docs
|
|
||||||
* app.docs({basePath: 'http://localhost:3000'});
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Run your app then navigate to
|
|
||||||
* [the API explorer](http://petstore.swagger.wordnik.com/).
|
|
||||||
* Enter your API basepath to view your generated docs.
|
|
||||||
*
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
|
|
||||||
app.docs = function (options) {
|
|
||||||
var remotes = this.remotes();
|
|
||||||
swagger(remotes, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Get a handler of the specified type from the handler cache.
|
* Get a handler of the specified type from the handler cache.
|
||||||
*/
|
*/
|
||||||
|
@ -381,207 +344,9 @@ app.enableAuth = function() {
|
||||||
this.isAuthEnabled = true;
|
this.isAuthEnabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize an application from an options object or a set of JSON and JavaScript files.
|
|
||||||
*
|
|
||||||
* This function takes an optional argument that is either a string or an object.
|
|
||||||
*
|
|
||||||
* If the argument is a string, then it sets the application root directory based on the string value. Then it:
|
|
||||||
* 1. Creates DataSources from the `datasources.json` file in the application root directory.
|
|
||||||
* 2. Creates Models from the `models.json` file in the application root directory.
|
|
||||||
*
|
|
||||||
* If the argument is an object, then it looks for `model`, `dataSources`, and `appRootDir` properties of the object.
|
|
||||||
* If the object has no `appRootDir` property then it sets the current working directory as the application root directory.
|
|
||||||
* Then it:
|
|
||||||
* 1. Creates DataSources from the `options.dataSources` object.
|
|
||||||
* 2. Creates Models from the `options.models` object.
|
|
||||||
*
|
|
||||||
* In both cases, the function loads JavaScript files in the `/models` and `/boot` subdirectories of the application root directory with `require()`.
|
|
||||||
*
|
|
||||||
* **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
|
|
||||||
* files may result in models being **undefined** due to race conditions.
|
|
||||||
* To avoid this when using `app.boot()` make sure all models are passed as part of the `models` definition.
|
|
||||||
*
|
|
||||||
* Throws an error if the config object is not valid or if boot fails.
|
|
||||||
*
|
|
||||||
* <a name="model-definition"></a>
|
|
||||||
* **Model Definitions**
|
|
||||||
*
|
|
||||||
* The following is example JSON for two `Model` definitions: "dealership" and "location".
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* {
|
|
||||||
* "dealership": {
|
|
||||||
* // a reference, by name, to a dataSource definition
|
|
||||||
* "dataSource": "my-db",
|
|
||||||
* // the options passed to Model.extend(name, properties, options)
|
|
||||||
* "options": {
|
|
||||||
* "relations": {
|
|
||||||
* "cars": {
|
|
||||||
* "type": "hasMany",
|
|
||||||
* "model": "Car",
|
|
||||||
* "foreignKey": "dealerId"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* // the properties passed to Model.extend(name, properties, options)
|
|
||||||
* "properties": {
|
|
||||||
* "id": {"id": true},
|
|
||||||
* "name": "String",
|
|
||||||
* "zip": "Number",
|
|
||||||
* "address": "String"
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* "car": {
|
|
||||||
* "dataSource": "my-db"
|
|
||||||
* "properties": {
|
|
||||||
* "id": {
|
|
||||||
* "type": "String",
|
|
||||||
* "required": true,
|
|
||||||
* "id": true
|
|
||||||
* },
|
|
||||||
* "make": {
|
|
||||||
* "type": "String",
|
|
||||||
* "required": true
|
|
||||||
* },
|
|
||||||
* "model": {
|
|
||||||
* "type": "String",
|
|
||||||
* "required": true
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @options {String|Object} options Boot options; If String, this is the application root directory; if object, has below properties.
|
|
||||||
* @property {String} appRootDir Directory to use when loading JSON and JavaScript files (optional). Defaults to the current directory (`process.cwd()`).
|
|
||||||
* @property {Object} models Object containing `Model` definitions (optional).
|
|
||||||
* @property {Object} dataSources Object containing `DataSource` definitions (optional).
|
|
||||||
* @end
|
|
||||||
*
|
|
||||||
* @header app.boot([options])
|
|
||||||
*/
|
|
||||||
|
|
||||||
app.boot = function(options) {
|
app.boot = function(options) {
|
||||||
options = options || {};
|
throw new Error(
|
||||||
|
'`app.boot` was removed, use the new module loopback-boot instead');
|
||||||
if(typeof options === 'string') {
|
|
||||||
options = {appRootDir: options};
|
|
||||||
}
|
|
||||||
var app = this;
|
|
||||||
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
|
|
||||||
var ctx = {};
|
|
||||||
var appConfig = options.app;
|
|
||||||
var modelConfig = options.models;
|
|
||||||
var dataSourceConfig = options.dataSources;
|
|
||||||
|
|
||||||
if(!appConfig) {
|
|
||||||
appConfig = tryReadConfig(appRootDir, 'app') || {};
|
|
||||||
}
|
|
||||||
if(!modelConfig) {
|
|
||||||
modelConfig = tryReadConfig(appRootDir, 'models') || {};
|
|
||||||
}
|
|
||||||
if(!dataSourceConfig) {
|
|
||||||
dataSourceConfig = tryReadConfig(appRootDir, 'datasources') || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
assertIsValidConfig('app', appConfig);
|
|
||||||
assertIsValidConfig('model', modelConfig);
|
|
||||||
assertIsValidConfig('data source', dataSourceConfig);
|
|
||||||
|
|
||||||
appConfig.host =
|
|
||||||
process.env.npm_config_host ||
|
|
||||||
process.env.OPENSHIFT_SLS_IP ||
|
|
||||||
process.env.OPENSHIFT_NODEJS_IP ||
|
|
||||||
process.env.HOST ||
|
|
||||||
appConfig.host ||
|
|
||||||
process.env.npm_package_config_host ||
|
|
||||||
app.get('host');
|
|
||||||
|
|
||||||
appConfig.port = _.find([
|
|
||||||
process.env.npm_config_port,
|
|
||||||
process.env.OPENSHIFT_SLS_PORT,
|
|
||||||
process.env.OPENSHIFT_NODEJS_PORT,
|
|
||||||
process.env.PORT,
|
|
||||||
appConfig.port,
|
|
||||||
process.env.npm_package_config_port,
|
|
||||||
app.get('port'),
|
|
||||||
3000
|
|
||||||
], _.isFinite);
|
|
||||||
|
|
||||||
appConfig.restApiRoot =
|
|
||||||
appConfig.restApiRoot ||
|
|
||||||
app.get('restApiRoot') ||
|
|
||||||
'/api';
|
|
||||||
|
|
||||||
if(appConfig.host !== undefined) {
|
|
||||||
assert(typeof appConfig.host === 'string', 'app.host must be a string');
|
|
||||||
app.set('host', appConfig.host);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(appConfig.port !== undefined) {
|
|
||||||
var portType = typeof appConfig.port;
|
|
||||||
assert(portType === 'string' || portType === 'number', 'app.port must be a string or number');
|
|
||||||
app.set('port', appConfig.port);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(appConfig.restApiRoot !== undefined, 'app.restApiRoot is required');
|
|
||||||
assert(typeof appConfig.restApiRoot === 'string', 'app.restApiRoot must be a string');
|
|
||||||
assert(/^\//.test(appConfig.restApiRoot), 'app.restApiRoot must start with "/"');
|
|
||||||
app.set('restApiRoot', appConfig.restApiRoot);
|
|
||||||
|
|
||||||
for(var configKey in appConfig) {
|
|
||||||
var cur = app.get(configKey);
|
|
||||||
if(cur === undefined || cur === null) {
|
|
||||||
app.set(configKey, appConfig[configKey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// instantiate data sources
|
|
||||||
forEachKeyedObject(dataSourceConfig, function(key, obj) {
|
|
||||||
app.dataSource(key, obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
// instantiate models
|
|
||||||
forEachKeyedObject(modelConfig, function(key, obj) {
|
|
||||||
app.model(key, obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
// try to attach models to dataSources by type
|
|
||||||
try {
|
|
||||||
registry.autoAttach();
|
|
||||||
} catch(e) {
|
|
||||||
if(e.name === 'AssertionError') {
|
|
||||||
console.warn(e);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable token requirement for swagger, if available
|
|
||||||
var swagger = app.remotes().exports.swagger;
|
|
||||||
var requireTokenForSwagger = appConfig.swagger
|
|
||||||
&& appConfig.swagger.requireToken;
|
|
||||||
if(swagger) {
|
|
||||||
swagger.requireToken = requireTokenForSwagger || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// require directories
|
|
||||||
var requiredModels = requireDir(path.join(appRootDir, 'models'));
|
|
||||||
var requiredBootScripts = requireDir(path.join(appRootDir, 'boot'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertIsValidConfig(name, config) {
|
|
||||||
if(config) {
|
|
||||||
assert(typeof config === 'object', name + ' config must be a valid JSON object');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forEachKeyedObject(obj, fn) {
|
|
||||||
if(typeof obj !== 'object') return;
|
|
||||||
|
|
||||||
Object.keys(obj).forEach(function(key) {
|
|
||||||
fn(key, obj[key]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function classify(str) {
|
function classify(str) {
|
||||||
|
@ -634,216 +399,10 @@ function configureModel(ModelCtor, config, app) {
|
||||||
registry.configureModel(ModelCtor, config);
|
registry.configureModel(ModelCtor, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireDir(dir, basenames) {
|
|
||||||
assert(dir, 'cannot require directory contents without directory name');
|
|
||||||
|
|
||||||
var requires = {};
|
|
||||||
|
|
||||||
if (arguments.length === 2) {
|
|
||||||
// if basenames argument is passed, explicitly include those files
|
|
||||||
basenames.forEach(function (basename) {
|
|
||||||
var filepath = Path.resolve(Path.join(dir, basename));
|
|
||||||
requires[basename] = tryRequire(filepath);
|
|
||||||
});
|
|
||||||
} else if (arguments.length === 1) {
|
|
||||||
// if basenames arguments isn't passed, require all javascript
|
|
||||||
// files (except for those prefixed with _) and all directories
|
|
||||||
|
|
||||||
var files = tryReadDir(dir);
|
|
||||||
|
|
||||||
// sort files in lowercase alpha for linux
|
|
||||||
files.sort(function (a,b) {
|
|
||||||
a = a.toLowerCase();
|
|
||||||
b = b.toLowerCase();
|
|
||||||
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
} else if (b < a) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
files.forEach(function (filename) {
|
|
||||||
// ignore index.js and files prefixed with underscore
|
|
||||||
if ((filename === 'index.js') || (filename[0] === '_')) { return; }
|
|
||||||
|
|
||||||
var filepath = path.resolve(path.join(dir, filename));
|
|
||||||
var ext = path.extname(filename);
|
|
||||||
var stats = fs.statSync(filepath);
|
|
||||||
|
|
||||||
// only require files supported by require.extensions (.txt .md etc.)
|
|
||||||
if (stats.isFile() && !(ext in require.extensions)) { return; }
|
|
||||||
|
|
||||||
var basename = path.basename(filename, ext);
|
|
||||||
|
|
||||||
requires[basename] = tryRequire(filepath);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return requires;
|
|
||||||
};
|
|
||||||
|
|
||||||
function tryRequire(modulePath) {
|
|
||||||
try {
|
|
||||||
return require.apply(this, arguments);
|
|
||||||
} catch(e) {
|
|
||||||
console.error('failed to require "%s"', modulePath);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryReadDir() {
|
|
||||||
try {
|
|
||||||
return fs.readdirSync.apply(fs, arguments);
|
|
||||||
} catch(e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryReadConfig(cwd, fileName) {
|
|
||||||
try {
|
|
||||||
return require(path.join(cwd, fileName + '.json'));
|
|
||||||
} catch(e) {
|
|
||||||
if(e.code !== "MODULE_NOT_FOUND") {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearHandlerCache(app) {
|
function clearHandlerCache(app) {
|
||||||
app._handlers = undefined;
|
app._handlers = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* This function is now deprecated.
|
|
||||||
* Install all express middleware required by LoopBack.
|
|
||||||
*
|
|
||||||
* It is possible to inject your own middleware by listening on one of the
|
|
||||||
* following events:
|
|
||||||
*
|
|
||||||
* - `middleware:preprocessors` is emitted after all other
|
|
||||||
* request-preprocessing middleware was installed, but before any
|
|
||||||
* request-handling middleware is configured.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```js
|
|
||||||
* app.once('middleware:preprocessors', function() {
|
|
||||||
* app.use(loopback.limit('5.5mb'))
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
* - `middleware:handlers` is emitted when it's time to add your custom
|
|
||||||
* request-handling middleware. Note that you should not install any
|
|
||||||
* express routes at this point (express routes are discussed later).
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```js
|
|
||||||
* app.once('middleware:handlers', function() {
|
|
||||||
* app.use('/admin', adminExpressApp);
|
|
||||||
* app.use('/custom', function(req, res, next) {
|
|
||||||
* res.send(200, { url: req.url });
|
|
||||||
* });
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
* - `middleware:error-loggers` is emitted at the end, before the loopback
|
|
||||||
* error handling middleware is installed. This is the point where you
|
|
||||||
* can install your own middleware to log errors.
|
|
||||||
*
|
|
||||||
* Notes:
|
|
||||||
* - The middleware function must take four parameters, otherwise it won't
|
|
||||||
* be called by express.
|
|
||||||
*
|
|
||||||
* - It should also call `next(err)` to let the loopback error handler convert
|
|
||||||
* the error to an HTTP error response.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```js
|
|
||||||
* var bunyan = require('bunyan');
|
|
||||||
* var log = bunyan.createLogger({name: "myapp"});
|
|
||||||
* app.once('middleware:error-loggers', function() {
|
|
||||||
* app.use(function(err, req, res, next) {
|
|
||||||
* log.error(err);
|
|
||||||
* next(err);
|
|
||||||
* });
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Express routes should be added after `installMiddleware` was called.
|
|
||||||
* This way the express router middleware is injected at the right place in the
|
|
||||||
* middleware chain. If you add an express route before calling this function,
|
|
||||||
* bad things will happen: Express will automatically add the router
|
|
||||||
* middleware and since we haven't added request-preprocessing middleware like
|
|
||||||
* cookie & body parser yet, your route handlers will receive raw unprocessed
|
|
||||||
* requests.
|
|
||||||
*
|
|
||||||
* This is the correct order in which to call `app` methods:
|
|
||||||
* ```js
|
|
||||||
* app.boot(__dirname); // optional
|
|
||||||
*
|
|
||||||
* app.installMiddleware();
|
|
||||||
*
|
|
||||||
* // [register your express routes here]
|
|
||||||
*
|
|
||||||
* app.listen();
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
app.installMiddleware = function() {
|
|
||||||
var loopback = require('../');
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Request pre-processing
|
|
||||||
*/
|
|
||||||
this.use(loopback.favicon());
|
|
||||||
// TODO(bajtos) refactor to app.get('loggerFormat')
|
|
||||||
var loggerFormat = this.get('env') === 'development' ? 'dev' : 'default';
|
|
||||||
this.use(loopback.logger(loggerFormat));
|
|
||||||
this.use(loopback.cookieParser(this.get('cookieSecret')));
|
|
||||||
this.use(loopback.token({ model: this.models.accessToken }));
|
|
||||||
this.use(loopback.bodyParser());
|
|
||||||
this.use(loopback.methodOverride());
|
|
||||||
|
|
||||||
// Allow the app to install custom preprocessing middleware
|
|
||||||
this.emit('middleware:preprocessors');
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Request handling
|
|
||||||
*/
|
|
||||||
|
|
||||||
// LoopBack REST transport
|
|
||||||
this.use(this.get('restApiRoot') || '/api', loopback.rest());
|
|
||||||
|
|
||||||
// Allow the app to install custom request handling middleware
|
|
||||||
this.emit('middleware:handlers');
|
|
||||||
|
|
||||||
// Let express routes handle requests that were not handled
|
|
||||||
// by any of the middleware registered above.
|
|
||||||
// This way LoopBack REST and API Explorer take precedence over
|
|
||||||
// express routes.
|
|
||||||
this.use(this.router);
|
|
||||||
|
|
||||||
// The static file server should come after all other routes
|
|
||||||
// Every request that goes through the static middleware hits
|
|
||||||
// the file system to check if a file exists.
|
|
||||||
this.use(loopback.static(path.join(__dirname, 'public')));
|
|
||||||
|
|
||||||
// Requests that get this far won't be handled
|
|
||||||
// by any middleware. Convert them into a 404 error
|
|
||||||
// that will be handled later down the chain.
|
|
||||||
this.use(loopback.urlNotFound());
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Error handling
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Allow the app to install custom error logging middleware
|
|
||||||
this.emit('middleware:error-handlers');
|
|
||||||
|
|
||||||
// The ultimate error handler.
|
|
||||||
this.use(loopback.errorHandler());
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for connections and update the configured port.
|
* Listen for connections and update the configured port.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
var assert = require('assert');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compatibility layer allowing applications based on an older LoopBack version
|
|
||||||
* to work with newer versions with minimum changes involved.
|
|
||||||
*
|
|
||||||
* You should not use it unless migrating from an older version of LoopBack.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var compat = exports;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LoopBack versions pre-1.6 use plural model names when registering shared
|
|
||||||
* classes with strong-remoting. As the result, strong-remoting use method names
|
|
||||||
* like `Users.create` for the javascript methods like `User.create`.
|
|
||||||
* This has been fixed in v1.6, LoopBack consistently uses the singular
|
|
||||||
* form now.
|
|
||||||
*
|
|
||||||
* Turn this option on to enable the old behaviour.
|
|
||||||
*
|
|
||||||
* - `app.remotes()` and `app.remoteObjects()` will be indexed using
|
|
||||||
* plural names (Users instead of User).
|
|
||||||
*
|
|
||||||
* - Remote hooks must use plural names for the class name, i.e
|
|
||||||
* `Users.create` instead of `User.create`. This is transparently
|
|
||||||
* handled by `Model.beforeRemote()` and `Model.afterRemote()`.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @deprecated Your application should not depend on the way how loopback models
|
|
||||||
* and strong-remoting are wired together. It if does, you should update
|
|
||||||
* it to use singular model names.
|
|
||||||
*/
|
|
||||||
|
|
||||||
compat.usePluralNamesForRemoting = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the class name to use with strong-remoting.
|
|
||||||
* @param {function} Ctor Model class (constructor), e.g. `User`
|
|
||||||
* @return {string} Singular or plural name, depending on the value
|
|
||||||
* of `compat.usePluralNamesForRemoting`
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
|
|
||||||
compat.getClassNameForRemoting = function(Ctor) {
|
|
||||||
assert(
|
|
||||||
typeof(Ctor) === 'function',
|
|
||||||
'compat.getClassNameForRemoting expects a constructor as the argument');
|
|
||||||
|
|
||||||
if (compat.usePluralNamesForRemoting) {
|
|
||||||
assert(Ctor.pluralModelName,
|
|
||||||
'Model must have a "pluralModelName" property in compat mode');
|
|
||||||
return Ctor.pluralModelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ctor.modelName;
|
|
||||||
};
|
|
|
@ -51,4 +51,4 @@ Connector._createJDBAdapter = function (jdbModule) {
|
||||||
|
|
||||||
Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) {
|
Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,12 @@ MailConnector.prototype.setupTransport = function(setting) {
|
||||||
var connector = this;
|
var connector = this;
|
||||||
connector.transports = connector.transports || [];
|
connector.transports = connector.transports || [];
|
||||||
connector.transportsIndex = connector.transportsIndex || {};
|
connector.transportsIndex = connector.transportsIndex || {};
|
||||||
var transport = mailer.createTransport(setting.type, setting);
|
|
||||||
|
var transportModuleName = 'nodemailer-' + (setting.type || 'STUB').toLowerCase() + '-transport';
|
||||||
|
var transportModule = require(transportModuleName);
|
||||||
|
|
||||||
|
var transport = mailer.createTransport(transportModule(setting));
|
||||||
|
|
||||||
connector.transportsIndex[setting.type] = transport;
|
connector.transportsIndex[setting.type] = transport;
|
||||||
connector.transports.push(transport);
|
connector.transports.push(transport);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,4 +36,4 @@ inherits(Memory, Connector);
|
||||||
* JugglingDB Compatibility
|
* JugglingDB Compatibility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Memory.initialize = JdbMemory.initialize;
|
Memory.initialize = JdbMemory.initialize;
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
* Dependencies.
|
* Dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var assert = require('assert')
|
var assert = require('assert');
|
||||||
, compat = require('../compat')
|
var remoting = require('strong-remoting');
|
||||||
, _ = require('underscore');
|
var DataAccessObject = require('loopback-datasource-juggler/lib/dao');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the RemoteConnector class.
|
* Export the RemoteConnector class.
|
||||||
|
@ -24,6 +24,10 @@ function RemoteConnector(settings) {
|
||||||
this.root = settings.root || '';
|
this.root = settings.root || '';
|
||||||
this.host = settings.host || 'localhost';
|
this.host = settings.host || 'localhost';
|
||||||
this.port = settings.port || 3000;
|
this.port = settings.port || 3000;
|
||||||
|
this.remotes = remoting.create();
|
||||||
|
|
||||||
|
// TODO(ritch) make sure this name works with Model.getSourceId()
|
||||||
|
this.name = 'remote-connector';
|
||||||
|
|
||||||
if(settings.url) {
|
if(settings.url) {
|
||||||
this.url = settings.url;
|
this.url = settings.url;
|
||||||
|
@ -31,14 +35,14 @@ function RemoteConnector(settings) {
|
||||||
this.url = this.protocol + '://' + this.host + ':' + this.port + this.root;
|
this.url = this.protocol + '://' + this.host + ':' + this.port + this.root;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle mixins here
|
// handle mixins in the define() method
|
||||||
this.DataAccessObject = function() {};
|
var DAO = this.DataAccessObject = function() {};
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteConnector.prototype.connect = function() {
|
RemoteConnector.prototype.connect = function() {
|
||||||
|
this.remotes.connect(this.url, this.adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RemoteConnector.initialize = function(dataSource, callback) {
|
RemoteConnector.initialize = function(dataSource, callback) {
|
||||||
var connector = dataSource.connector = new RemoteConnector(dataSource.settings);
|
var connector = dataSource.connector = new RemoteConnector(dataSource.settings);
|
||||||
connector.connect();
|
connector.connect();
|
||||||
|
@ -47,22 +51,40 @@ RemoteConnector.initialize = function(dataSource, callback) {
|
||||||
|
|
||||||
RemoteConnector.prototype.define = function(definition) {
|
RemoteConnector.prototype.define = function(definition) {
|
||||||
var Model = definition.model;
|
var Model = definition.model;
|
||||||
var className = compat.getClassNameForRemoting(Model);
|
var remotes = this.remotes;
|
||||||
var url = this.url;
|
var SharedClass;
|
||||||
var adapter = this.adapter;
|
|
||||||
|
|
||||||
Model.remotes(function(err, remotes) {
|
assert(Model.sharedClass, 'cannot attach ' + Model.modelName
|
||||||
var sharedClass = getSharedClass(remotes, className);
|
+ ' to a remote connector without a Model.sharedClass');
|
||||||
remotes.connect(url, adapter);
|
|
||||||
sharedClass
|
remotes.addClass(Model.sharedClass);
|
||||||
.methods()
|
|
||||||
.forEach(Model.createProxyMethod.bind(Model));
|
Model
|
||||||
});
|
.sharedClass
|
||||||
|
.methods()
|
||||||
|
.forEach(function(remoteMethod) {
|
||||||
|
// TODO(ritch) more elegant way of ignoring a nested shared class
|
||||||
|
if(remoteMethod.name !== 'Change'
|
||||||
|
&& remoteMethod.name !== 'Checkpoint') {
|
||||||
|
createProxyMethod(Model, remotes, remoteMethod);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSharedClass(remotes, className) {
|
function createProxyMethod(Model, remotes, remoteMethod) {
|
||||||
return _.find(remotes.classes(), function(sharedClass) {
|
var scope = remoteMethod.isStatic ? Model : Model.prototype;
|
||||||
return sharedClass.name === className;
|
var original = scope[remoteMethod.name];
|
||||||
});
|
|
||||||
|
scope[remoteMethod.name] = function remoteMethodProxy() {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
|
||||||
|
var callback;
|
||||||
|
if(lastArgIsFunc) {
|
||||||
|
callback = args.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
remotes.invoke(remoteMethod.stringName, args, callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
var express = require('express');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var middlewares = exports;
|
||||||
|
|
||||||
|
function safeRequire(m) {
|
||||||
|
try {
|
||||||
|
return require(m);
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMiddlewareNotInstalled(memberName, moduleName) {
|
||||||
|
return function () {
|
||||||
|
var msg = 'The middleware loopback.' + memberName + ' is not installed.\n' +
|
||||||
|
'Run `npm install --save ' + moduleName + '` to fix the problem.';
|
||||||
|
throw new Error(msg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var middlewareModules = {
|
||||||
|
"compress": "compression",
|
||||||
|
"timeout": "connect-timeout",
|
||||||
|
"cookieParser": "cookie-parser",
|
||||||
|
"cookieSession": "cookie-session",
|
||||||
|
"csrf": "csurf",
|
||||||
|
"errorHandler": "errorhandler",
|
||||||
|
"session": "express-session",
|
||||||
|
"methodOverride": "method-override",
|
||||||
|
"logger": "morgan",
|
||||||
|
"responseTime": "response-time",
|
||||||
|
"favicon": "serve-favicon",
|
||||||
|
"directory": "serve-index",
|
||||||
|
// "static": "serve-static",
|
||||||
|
"vhost": "vhost"
|
||||||
|
};
|
||||||
|
|
||||||
|
middlewares.bodyParser = safeRequire('body-parser');
|
||||||
|
middlewares.json = middlewares.bodyParser && middlewares.bodyParser.json;
|
||||||
|
middlewares.urlencoded = middlewares.bodyParser && middlewares.bodyParser.urlencoded;
|
||||||
|
|
||||||
|
for (var m in middlewareModules) {
|
||||||
|
var moduleName = middlewareModules[m];
|
||||||
|
middlewares[m] = safeRequire(moduleName) || createMiddlewareNotInstalled(m, moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve-favicon requires a path
|
||||||
|
var favicon = middlewares.favicon;
|
||||||
|
middlewares.favicon = function (icon, options) {
|
||||||
|
icon = icon || path.join(__dirname, '../favicon.ico');
|
||||||
|
return favicon(icon, options);
|
||||||
|
};
|
|
@ -6,11 +6,7 @@ var express = require('express')
|
||||||
, proto = require('./application')
|
, proto = require('./application')
|
||||||
, fs = require('fs')
|
, fs = require('fs')
|
||||||
, ejs = require('ejs')
|
, ejs = require('ejs')
|
||||||
, EventEmitter = require('events').EventEmitter
|
|
||||||
, path = require('path')
|
, path = require('path')
|
||||||
, DataSource = require('loopback-datasource-juggler').DataSource
|
|
||||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
|
||||||
, i8n = require('inflection')
|
|
||||||
, merge = require('util')._extend
|
, merge = require('util')._extend
|
||||||
, assert = require('assert');
|
, assert = require('assert');
|
||||||
|
|
||||||
|
@ -42,11 +38,6 @@ loopback.version = require('../package.json').version;
|
||||||
|
|
||||||
loopback.mime = express.mime;
|
loopback.mime = express.mime;
|
||||||
|
|
||||||
/*!
|
|
||||||
* Compatibility layer, intentionally left undocumented.
|
|
||||||
*/
|
|
||||||
loopback.compat = require('./compat');
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Create an loopback application.
|
* Create an loopback application.
|
||||||
*
|
*
|
||||||
|
@ -84,6 +75,10 @@ function createApplication() {
|
||||||
function mixin(source) {
|
function mixin(source) {
|
||||||
for (var key in source) {
|
for (var key in source) {
|
||||||
var desc = Object.getOwnPropertyDescriptor(source, key);
|
var desc = Object.getOwnPropertyDescriptor(source, key);
|
||||||
|
|
||||||
|
// Fix for legacy (pre-ES5) browsers like PhantomJS
|
||||||
|
if (!desc) continue;
|
||||||
|
|
||||||
Object.defineProperty(loopback, key, desc);
|
Object.defineProperty(loopback, key, desc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,12 +87,22 @@ mixin(require('./runtime'));
|
||||||
mixin(require('./registry'));
|
mixin(require('./registry'));
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Expose express.middleware as loopback.*
|
* Expose static express methods like `express.errorHandler`.
|
||||||
* for example `loopback.errorHandler` etc.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
mixin(express);
|
mixin(express);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Expose additional middleware like session as loopback.*
|
||||||
|
* This will keep the loopback API compatible with express 3.x
|
||||||
|
*
|
||||||
|
* ***only in node***
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (loopback.isServer) {
|
||||||
|
var middlewares = require('./express-middleware');
|
||||||
|
mixin(middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Expose additional loopback middleware
|
* Expose additional loopback middleware
|
||||||
|
@ -168,6 +173,7 @@ loopback.Role = require('./models/role').Role;
|
||||||
loopback.RoleMapping = require('./models/role').RoleMapping;
|
loopback.RoleMapping = require('./models/role').RoleMapping;
|
||||||
loopback.ACL = require('./models/acl').ACL;
|
loopback.ACL = require('./models/acl').ACL;
|
||||||
loopback.Scope = require('./models/acl').Scope;
|
loopback.Scope = require('./models/acl').Scope;
|
||||||
|
loopback.Change = require('./models/change');
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Automatically attach these models to dataSources
|
* Automatically attach these models to dataSources
|
||||||
|
@ -179,7 +185,7 @@ var dataSourceTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
||||||
loopback.DataModel.autoAttach = dataSourceTypes.DB;
|
loopback.PersistedModel.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.User.autoAttach = dataSourceTypes.DB;
|
loopback.User.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.Role.autoAttach = dataSourceTypes.DB;
|
loopback.Role.autoAttach = dataSourceTypes.DB;
|
||||||
|
|
|
@ -92,7 +92,7 @@ var ACLSchema = {
|
||||||
* @inherits Model
|
* @inherits Model
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ACL = loopback.createModel('ACL', ACLSchema);
|
var ACL = loopback.PersistedModel.extend('ACL', ACLSchema);
|
||||||
|
|
||||||
ACL.ALL = AccessContext.ALL;
|
ACL.ALL = AccessContext.ALL;
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
* @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.getModel(model);
|
var modelClass = loopback.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) {
|
||||||
|
@ -343,7 +343,7 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||||
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.getModel(model);
|
var modelClass = loopback.findModel(model);
|
||||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
||||||
}
|
}
|
||||||
callback && callback(null, resolved);
|
callback && callback(null, resolved);
|
||||||
|
|
|
@ -113,7 +113,7 @@ function generateKey(hmacKey, algorithm, encoding) {
|
||||||
* @inherits {Model}
|
* @inherits {Model}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Application = loopback.createModel('Application', ApplicationSchema);
|
var Application = loopback.PersistedModel.extend('Application', ApplicationSchema);
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* A hook to generate keys before creation
|
* A hook to generate keys before creation
|
||||||
|
|
|
@ -0,0 +1,646 @@
|
||||||
|
/**
|
||||||
|
* Module Dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var PersistedModel = require('./persisted-model')
|
||||||
|
, loopback = require('../loopback')
|
||||||
|
, crypto = require('crypto')
|
||||||
|
, CJSON = {stringify: require('canonical-json')}
|
||||||
|
, async = require('async')
|
||||||
|
, assert = require('assert')
|
||||||
|
, debug = require('debug')('loopback:change');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
var properties = {
|
||||||
|
id: {type: String, generated: true, id: true},
|
||||||
|
rev: {type: String},
|
||||||
|
prev: {type: String},
|
||||||
|
checkpoint: {type: Number},
|
||||||
|
modelName: {type: String},
|
||||||
|
modelId: {type: String}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options
|
||||||
|
*/
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
trackChanges: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change list entry.
|
||||||
|
*
|
||||||
|
* @property id {String} Hash of the modelName and id
|
||||||
|
* @property rev {String} the current model revision
|
||||||
|
* @property prev {String} the previous model revision
|
||||||
|
* @property checkpoint {Number} the current checkpoint at time of the change
|
||||||
|
* @property modelName {String} the model name
|
||||||
|
* @property modelId {String} the model id
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @inherits {Model}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Change = module.exports = PersistedModel.extend('Change', properties, options);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.UPDATE = 'update';
|
||||||
|
Change.CREATE = 'create';
|
||||||
|
Change.DELETE = 'delete';
|
||||||
|
Change.UNKNOWN = 'unknown';
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Conflict Class
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.Conflict = Conflict;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Setup the extended model.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.setup = function() {
|
||||||
|
PersistedModel.setup.call(this);
|
||||||
|
var Change = this;
|
||||||
|
|
||||||
|
Change.getter.id = function() {
|
||||||
|
var hasModel = this.modelName && this.modelId;
|
||||||
|
if(!hasModel) return null;
|
||||||
|
|
||||||
|
return Change.idForModel(this.modelName, this.modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Change.setup();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track the recent change of the given modelIds.
|
||||||
|
*
|
||||||
|
* @param {String} modelName
|
||||||
|
* @param {Array} modelIds
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Array} changes Changes that were tracked
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
||||||
|
var tasks = [];
|
||||||
|
var Change = this;
|
||||||
|
|
||||||
|
modelIds.forEach(function(id) {
|
||||||
|
tasks.push(function(cb) {
|
||||||
|
Change.findOrCreateChange(modelName, id, function(err, change) {
|
||||||
|
if(err) return Change.handleError(err, cb);
|
||||||
|
change.rectify(cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
async.parallel(tasks, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an identifier for a given model.
|
||||||
|
*
|
||||||
|
* @param {String} modelName
|
||||||
|
* @param {String} modelId
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.idForModel = function(modelName, modelId) {
|
||||||
|
return this.hash([modelName, modelId].join('-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find or create a change for the given model.
|
||||||
|
*
|
||||||
|
* @param {String} modelName
|
||||||
|
* @param {String} modelId
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Change} change
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||||
|
assert(loopback.findModel(modelName), modelName + ' does not exist');
|
||||||
|
var id = this.idForModel(modelName, modelId);
|
||||||
|
var Change = this;
|
||||||
|
|
||||||
|
this.findById(id, function(err, change) {
|
||||||
|
if(err) return callback(err);
|
||||||
|
if(change) {
|
||||||
|
callback(null, change);
|
||||||
|
} else {
|
||||||
|
var ch = new Change({
|
||||||
|
id: id,
|
||||||
|
modelName: modelName,
|
||||||
|
modelId: modelId
|
||||||
|
});
|
||||||
|
ch.debug('creating change');
|
||||||
|
ch.save(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update (or create) the change with the current revision.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Change} change
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.rectify = function(cb) {
|
||||||
|
var change = this;
|
||||||
|
var tasks = [
|
||||||
|
updateRevision,
|
||||||
|
updateCheckpoint
|
||||||
|
];
|
||||||
|
var currentRev = this.rev;
|
||||||
|
|
||||||
|
change.debug('rectify change');
|
||||||
|
|
||||||
|
cb = cb || function(err) {
|
||||||
|
if(err) throw new Error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.parallel(tasks, function(err) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
if(change.prev === Change.UNKNOWN) {
|
||||||
|
// this occurs when a record of a change doesn't exist
|
||||||
|
// and its current revision is null (not found)
|
||||||
|
change.remove(cb);
|
||||||
|
} else {
|
||||||
|
change.save(cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateRevision(cb) {
|
||||||
|
// get the current revision
|
||||||
|
change.currentRevision(function(err, rev) {
|
||||||
|
if(err) return Change.handleError(err, cb);
|
||||||
|
if(rev) {
|
||||||
|
// avoid setting rev and prev to the same value
|
||||||
|
if(currentRev !== rev) {
|
||||||
|
change.rev = rev;
|
||||||
|
change.prev = currentRev;
|
||||||
|
} else {
|
||||||
|
change.debug('rev and prev are equal (not updating rev)');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
change.rev = null;
|
||||||
|
if(currentRev) {
|
||||||
|
change.prev = currentRev;
|
||||||
|
} else if(!change.prev) {
|
||||||
|
change.debug('ERROR - could not determing prev');
|
||||||
|
change.prev = Change.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
change.debug('updated revision (was ' + currentRev + ')');
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCheckpoint(cb) {
|
||||||
|
change.constructor.getCheckpointModel().current(function(err, checkpoint) {
|
||||||
|
if(err) return Change.handleError(err);
|
||||||
|
change.checkpoint = checkpoint;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a change's current revision based on current data.
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {String} rev The current revision
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.currentRevision = function(cb) {
|
||||||
|
var model = this.getModelCtor();
|
||||||
|
var id = this.getModelId();
|
||||||
|
model.findById(id, function(err, inst) {
|
||||||
|
if(err) return Change.handleError(err, cb);
|
||||||
|
if(inst) {
|
||||||
|
cb(null, Change.revisionForInst(inst));
|
||||||
|
} else {
|
||||||
|
cb(null, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a hash of the given `string` with the `options.hashAlgorithm`.
|
||||||
|
* **Default: `sha1`**
|
||||||
|
*
|
||||||
|
* @param {String} str The string to be hashed
|
||||||
|
* @return {String} The hashed string
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.hash = function(str) {
|
||||||
|
return crypto
|
||||||
|
.createHash(Change.settings.hashAlgorithm || 'sha1')
|
||||||
|
.update(str)
|
||||||
|
.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the revision string for the given object
|
||||||
|
* @param {Object} inst The data to get the revision string for
|
||||||
|
* @return {String} The revision string
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.revisionForInst = function(inst) {
|
||||||
|
return this.hash(CJSON.stringify(inst));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a change's type. Returns one of:
|
||||||
|
*
|
||||||
|
* - `Change.UPDATE`
|
||||||
|
* - `Change.CREATE`
|
||||||
|
* - `Change.DELETE`
|
||||||
|
* - `Change.UNKNOWN`
|
||||||
|
*
|
||||||
|
* @return {String} the type of change
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.type = function() {
|
||||||
|
if(this.rev && this.prev) {
|
||||||
|
return Change.UPDATE;
|
||||||
|
}
|
||||||
|
if(this.rev && !this.prev) {
|
||||||
|
return Change.CREATE;
|
||||||
|
}
|
||||||
|
if(!this.rev && this.prev) {
|
||||||
|
return Change.DELETE;
|
||||||
|
}
|
||||||
|
return Change.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two changes.
|
||||||
|
* @param {Change} change
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.equals = function(change) {
|
||||||
|
if(!change) return false;
|
||||||
|
var thisRev = this.rev || null;
|
||||||
|
var thatRev = change.rev || null;
|
||||||
|
return thisRev === thatRev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does this change conflict with the given change.
|
||||||
|
* @param {Change} change
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.conflictsWith = function(change) {
|
||||||
|
if(!change) return false;
|
||||||
|
if(this.equals(change)) return false;
|
||||||
|
if(Change.bothDeleted(this, change)) return false;
|
||||||
|
if(this.isBasedOn(change)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are both changes deletes?
|
||||||
|
* @param {Change} a
|
||||||
|
* @param {Change} b
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.bothDeleted = function(a, b) {
|
||||||
|
return a.type() === Change.DELETE
|
||||||
|
&& b.type() === Change.DELETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the change is based on the given change.
|
||||||
|
* @param {Change} change
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.isBasedOn = function(change) {
|
||||||
|
return this.prev === change.rev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the differences for a given model since a given checkpoint.
|
||||||
|
*
|
||||||
|
* The callback will contain an error or `result`.
|
||||||
|
*
|
||||||
|
* **result**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* {
|
||||||
|
* deltas: Array,
|
||||||
|
* conflicts: Array
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **deltas**
|
||||||
|
*
|
||||||
|
* An array of changes that differ from `remoteChanges`.
|
||||||
|
*
|
||||||
|
* **conflicts**
|
||||||
|
*
|
||||||
|
* An array of changes that conflict with `remoteChanges`.
|
||||||
|
*
|
||||||
|
* @param {String} modelName
|
||||||
|
* @param {Number} since Compare changes after this checkpoint
|
||||||
|
* @param {Change[]} remoteChanges A set of changes to compare
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Object} result See above.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||||
|
var remoteChangeIndex = {};
|
||||||
|
var modelIds = [];
|
||||||
|
remoteChanges.forEach(function(ch) {
|
||||||
|
modelIds.push(ch.modelId);
|
||||||
|
remoteChangeIndex[ch.modelId] = new Change(ch);
|
||||||
|
});
|
||||||
|
|
||||||
|
// normalize `since`
|
||||||
|
since = Number(since) || 0;
|
||||||
|
this.find({
|
||||||
|
where: {
|
||||||
|
modelName: modelName,
|
||||||
|
modelId: {inq: modelIds},
|
||||||
|
checkpoint: {gte: since}
|
||||||
|
}
|
||||||
|
}, function(err, localChanges) {
|
||||||
|
if(err) return callback(err);
|
||||||
|
var deltas = [];
|
||||||
|
var conflicts = [];
|
||||||
|
var localModelIds = [];
|
||||||
|
|
||||||
|
localChanges.forEach(function(localChange) {
|
||||||
|
localChange = new Change(localChange);
|
||||||
|
localModelIds.push(localChange.modelId);
|
||||||
|
var remoteChange = remoteChangeIndex[localChange.modelId];
|
||||||
|
if(remoteChange && !localChange.equals(remoteChange)) {
|
||||||
|
if(remoteChange.conflictsWith(localChange)) {
|
||||||
|
remoteChange.debug('remote conflict');
|
||||||
|
localChange.debug('local conflict');
|
||||||
|
conflicts.push(localChange);
|
||||||
|
} else {
|
||||||
|
remoteChange.debug('remote delta');
|
||||||
|
deltas.push(remoteChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modelIds.forEach(function(id) {
|
||||||
|
if(localModelIds.indexOf(id) === -1) {
|
||||||
|
deltas.push(remoteChangeIndex[id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(null, {
|
||||||
|
deltas: deltas,
|
||||||
|
conflicts: conflicts
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct all change list entries.
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.rectifyAll = function(cb) {
|
||||||
|
debug('rectify all');
|
||||||
|
var Change = this;
|
||||||
|
// this should be optimized
|
||||||
|
this.find(function(err, changes) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
changes.forEach(function(change) {
|
||||||
|
change = new Change(change);
|
||||||
|
change.rectify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the checkpoint model.
|
||||||
|
* @return {Checkpoint}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.getCheckpointModel = function() {
|
||||||
|
var checkpointModel = this.Checkpoint;
|
||||||
|
if(checkpointModel) return checkpointModel;
|
||||||
|
this.checkpoint = checkpointModel = require('./checkpoint').extend('checkpoint');
|
||||||
|
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName
|
||||||
|
+ ' is not attached to a dataSource');
|
||||||
|
checkpointModel.attachTo(this.dataSource);
|
||||||
|
return checkpointModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Change.handleError = function(err) {
|
||||||
|
if(!this.settings.ignoreErrors) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Change.prototype.debug = function() {
|
||||||
|
if(debug.enabled) {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
debug.apply(this, args);
|
||||||
|
debug('\tid', this.id);
|
||||||
|
debug('\trev', this.rev);
|
||||||
|
debug('\tprev', this.prev);
|
||||||
|
debug('\tmodelName', this.modelName);
|
||||||
|
debug('\tmodelId', this.modelId);
|
||||||
|
debug('\ttype', this.type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the `Model` class for `change.modelName`.
|
||||||
|
* @return {Model}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Change.prototype.getModelCtor = function() {
|
||||||
|
return this.constructor.settings.trackModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Change.prototype.getModelId = function() {
|
||||||
|
// TODO(ritch) get rid of the need to create an instance
|
||||||
|
var Model = this.getModelCtor();
|
||||||
|
var id = this.modelId;
|
||||||
|
var m = new Model();
|
||||||
|
m.setId(id);
|
||||||
|
return m.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Change.prototype.getModel = function(callback) {
|
||||||
|
var Model = this.constructor.settings.trackModel;
|
||||||
|
var id = this.getModelId();
|
||||||
|
Model.findById(id, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When two changes conflict a conflict is created.
|
||||||
|
*
|
||||||
|
* **Note: call `conflict.fetch()` to get the `target` and `source` models.
|
||||||
|
*
|
||||||
|
* @param {*} modelId
|
||||||
|
* @param {PersistedModel} SourceModel
|
||||||
|
* @param {PersistedModel} TargetModel
|
||||||
|
* @property {ModelClass} source The source model instance
|
||||||
|
* @property {ModelClass} target The target model instance
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Conflict(modelId, SourceModel, TargetModel) {
|
||||||
|
this.SourceModel = SourceModel;
|
||||||
|
this.TargetModel = TargetModel;
|
||||||
|
this.SourceChange = SourceModel.getChangeModel();
|
||||||
|
this.TargetChange = TargetModel.getChangeModel();
|
||||||
|
this.modelId = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the conflicting models.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error}
|
||||||
|
* @param {PersistedModel} source
|
||||||
|
* @param {PersistedModel} target
|
||||||
|
*/
|
||||||
|
|
||||||
|
Conflict.prototype.models = function(cb) {
|
||||||
|
var conflict = this;
|
||||||
|
var SourceModel = this.SourceModel;
|
||||||
|
var TargetModel = this.TargetModel;
|
||||||
|
var source;
|
||||||
|
var target;
|
||||||
|
|
||||||
|
async.parallel([
|
||||||
|
getSourceModel,
|
||||||
|
getTargetModel
|
||||||
|
], done);
|
||||||
|
|
||||||
|
function getSourceModel(cb) {
|
||||||
|
SourceModel.findById(conflict.modelId, function(err, model) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
source = model;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetModel(cb) {
|
||||||
|
TargetModel.findById(conflict.modelId, function(err, model) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
target = model;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(err) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
cb(null, source, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the conflicting changes.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Change} sourceChange
|
||||||
|
* @param {Change} targetChange
|
||||||
|
*/
|
||||||
|
|
||||||
|
Conflict.prototype.changes = function(cb) {
|
||||||
|
var conflict = this;
|
||||||
|
var sourceChange;
|
||||||
|
var targetChange;
|
||||||
|
|
||||||
|
async.parallel([
|
||||||
|
getSourceChange,
|
||||||
|
getTargetChange
|
||||||
|
], done);
|
||||||
|
|
||||||
|
function getSourceChange(cb) {
|
||||||
|
conflict.SourceChange.findOne({where: {
|
||||||
|
modelId: conflict.modelId
|
||||||
|
}}, function(err, change) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
sourceChange = change;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetChange(cb) {
|
||||||
|
conflict.TargetChange.findOne({where: {
|
||||||
|
modelId: conflict.modelId
|
||||||
|
}}, function(err, change) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
targetChange = change;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(err) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
cb(null, sourceChange, targetChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the conflict.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
|
||||||
|
Conflict.prototype.resolve = function(cb) {
|
||||||
|
var conflict = this;
|
||||||
|
conflict.changes(function(err, sourceChange, targetChange) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
sourceChange.prev = targetChange.rev;
|
||||||
|
sourceChange.save(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the conflict type.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // possible results are
|
||||||
|
* Change.UPDATE // => source and target models were updated
|
||||||
|
* Change.DELETE // => the source and or target model was deleted
|
||||||
|
* Change.UNKNOWN // => the conflict type is uknown or due to an error
|
||||||
|
* ```
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {String} type The conflict type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Conflict.prototype.type = function(cb) {
|
||||||
|
var conflict = this;
|
||||||
|
this.changes(function(err, sourceChange, targetChange) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
var sourceChangeType = sourceChange.type();
|
||||||
|
var targetChangeType = targetChange.type();
|
||||||
|
if(sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
|
||||||
|
return cb(null, Change.UPDATE);
|
||||||
|
}
|
||||||
|
if(sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
|
||||||
|
return cb(null, Change.DELETE);
|
||||||
|
}
|
||||||
|
return cb(null, Change.UNKNOWN);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* Module Dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var PersistedModel = require('../loopback').PersistedModel
|
||||||
|
, loopback = require('../loopback')
|
||||||
|
, assert = require('assert');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
var properties = {
|
||||||
|
seq: {type: Number},
|
||||||
|
time: {type: Date, default: Date},
|
||||||
|
sourceId: {type: String}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options
|
||||||
|
*/
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkpoint list entry.
|
||||||
|
*
|
||||||
|
* @property id {Number} the sequencial identifier of a checkpoint
|
||||||
|
* @property time {Number} the time when the checkpoint was created
|
||||||
|
* @property sourceId {String} the source identifier
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @inherits {PersistedModel}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Checkpoint = module.exports = PersistedModel.extend('Checkpoint', properties, options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current checkpoint id
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Number} checkpointId The current checkpoint id
|
||||||
|
*/
|
||||||
|
|
||||||
|
Checkpoint.current = function(cb) {
|
||||||
|
var Checkpoint = this;
|
||||||
|
this.find({
|
||||||
|
limit: 1,
|
||||||
|
order: 'seq DESC'
|
||||||
|
}, function(err, checkpoints) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
var checkpoint = checkpoints[0];
|
||||||
|
if(checkpoint) {
|
||||||
|
cb(null, checkpoint.seq);
|
||||||
|
} else {
|
||||||
|
Checkpoint.create({seq: 0}, function(err, checkpoint) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
cb(null, checkpoint.seq);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Checkpoint.beforeSave = function(next, model) {
|
||||||
|
if(!model.getId() && model.seq === undefined) {
|
||||||
|
model.constructor.current(function(err, seq) {
|
||||||
|
if(err) return next(err);
|
||||||
|
model.seq = seq + 1;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,427 +0,0 @@
|
||||||
/*!
|
|
||||||
* Module Dependencies.
|
|
||||||
*/
|
|
||||||
var Model = require('./model');
|
|
||||||
var DataAccess = require('loopback-datasource-juggler/lib/dao');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends Model with basic query and CRUD support.
|
|
||||||
*
|
|
||||||
* **Change Event**
|
|
||||||
*
|
|
||||||
* Listen for model changes using the `change` event.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* MyDataModel.on('changed', function(obj) {
|
|
||||||
* console.log(obj) // => the changed model
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @class DataModel
|
|
||||||
* @param {Object} data
|
|
||||||
* @param {Number} data.id The default id property
|
|
||||||
*/
|
|
||||||
|
|
||||||
var DataModel = module.exports = Model.extend('DataModel');
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Configure the remoting attributes for a given function
|
|
||||||
* @param {Function} fn The function
|
|
||||||
* @param {Object} options The options
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function setRemoting(fn, options) {
|
|
||||||
options = options || {};
|
|
||||||
for (var opt in options) {
|
|
||||||
if (options.hasOwnProperty(opt)) {
|
|
||||||
fn[opt] = options[opt];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn.shared = true;
|
|
||||||
// allow connectors to override the function by marking as delegate
|
|
||||||
fn._delegate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Throw an error telling the user that the method is not available and why.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function throwNotAttached(modelName, methodName) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot call ' + modelName + '.'+ methodName + '().'
|
|
||||||
+ ' The ' + methodName + ' method has not been setup.'
|
|
||||||
+ ' The DataModel has not been correctly attached to a DataSource!'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Convert null callbacks to 404 error objects.
|
|
||||||
* @param {HttpContext} ctx
|
|
||||||
* @param {Function} cb
|
|
||||||
*/
|
|
||||||
|
|
||||||
function convertNullToNotFoundError(ctx, cb) {
|
|
||||||
if (ctx.result !== null) return cb();
|
|
||||||
|
|
||||||
var modelName = ctx.method.sharedClass.name;
|
|
||||||
var id = ctx.getArgByName('id');
|
|
||||||
var msg = 'Unknown "' + modelName + '" id "' + id + '".';
|
|
||||||
var error = new Error(msg);
|
|
||||||
error.statusCode = error.status = 404;
|
|
||||||
cb(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new instance of Model class, saved in database.
|
|
||||||
*
|
|
||||||
* @param {Object} [data] Object containing model instance data.
|
|
||||||
* @callback {Function} callback Callback function; see below.
|
|
||||||
* @param {Error|null} err Error object
|
|
||||||
* @param {Model|null} Model instance
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.create = function (data, callback) {
|
|
||||||
throwNotAttached(this.modelName, 'create');
|
|
||||||
};
|
|
||||||
|
|
||||||
setRemoting(DataModel.create, {
|
|
||||||
description: 'Create a new instance of the model and persist it into the data source',
|
|
||||||
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
|
|
||||||
returns: {arg: 'data', type: 'object', root: true},
|
|
||||||
http: {verb: 'post', path: '/'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update or insert a model instance
|
|
||||||
* @param {Object} data The model instance data
|
|
||||||
* @param {Function} [callback] The callback function
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.upsert = function upsert(data, callback) {
|
|
||||||
throwNotAttached(this.modelName, 'updateOrCreate');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for upsert function.
|
|
||||||
*/
|
|
||||||
DataModel.updateOrCreate = DataModel.upsert;
|
|
||||||
|
|
||||||
// upsert ~ remoting attributes
|
|
||||||
setRemoting(DataModel.upsert, {
|
|
||||||
description: 'Update an existing model instance or insert a new one into the data source',
|
|
||||||
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
|
|
||||||
returns: {arg: 'data', type: 'object', root: true},
|
|
||||||
http: {verb: 'put', path: '/'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find one record instance, same as `all`, limited by one and return object, not collection.
|
|
||||||
* If not found, create the record using data provided as second argument.
|
|
||||||
*
|
|
||||||
* @param {Object} query - search conditions: {where: {test: 'me'}}.
|
|
||||||
* @param {Object} data - object to create.
|
|
||||||
* @param {Function} cb - callback called with (err, instance)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.findOrCreate = function findOrCreate(query, data, callback) {
|
|
||||||
throwNotAttached(this.modelName, 'findOrCreate');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a model instance exists in database.
|
|
||||||
*
|
|
||||||
* @param {id} id - identifier of object (primary key value)
|
|
||||||
* @param {Function} cb - callbacl called with (err, exists: Bool)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.exists = function exists(id, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'exists');
|
|
||||||
};
|
|
||||||
|
|
||||||
// exists ~ remoting attributes
|
|
||||||
setRemoting(DataModel.exists, {
|
|
||||||
description: 'Check whether a model instance exists in the data source',
|
|
||||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
|
||||||
returns: {arg: 'exists', type: 'any'},
|
|
||||||
http: {verb: 'get', path: '/:id/exists'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find object by id
|
|
||||||
*
|
|
||||||
* @param {*} id - primary key value
|
|
||||||
* @param {Function} cb - callback called with (err, instance)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.findById = function find(id, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'find');
|
|
||||||
};
|
|
||||||
|
|
||||||
// find ~ remoting attributes
|
|
||||||
setRemoting(DataModel.findById, {
|
|
||||||
description: 'Find a model instance by id from the data source',
|
|
||||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
|
||||||
returns: {arg: 'data', type: 'any', root: true},
|
|
||||||
http: {verb: 'get', path: '/:id'},
|
|
||||||
rest: {after: convertNullToNotFoundError}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all instances of Model, matched by query
|
|
||||||
* make sure you have marked as `index: true` fields for filter or sort
|
|
||||||
*
|
|
||||||
* @param {Object} params (optional)
|
|
||||||
*
|
|
||||||
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
|
|
||||||
* - include: String, Object or Array. See DataModel.include documentation.
|
|
||||||
* - order: String
|
|
||||||
* - limit: Number
|
|
||||||
* - skip: Number
|
|
||||||
*
|
|
||||||
* @param {Function} callback (required) called with arguments:
|
|
||||||
*
|
|
||||||
* - err (null or Error)
|
|
||||||
* - Array of instances
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.find = function find(params, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'find');
|
|
||||||
};
|
|
||||||
|
|
||||||
// all ~ remoting attributes
|
|
||||||
setRemoting(DataModel.find, {
|
|
||||||
description: 'Find all instances of the model matched by filter from the data source',
|
|
||||||
accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'},
|
|
||||||
returns: {arg: 'data', type: 'array', root: true},
|
|
||||||
http: {verb: 'get', path: '/'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find one record, same as `all`, limited by 1 and return object, not collection
|
|
||||||
*
|
|
||||||
* @param {Object} params - search conditions: {where: {test: 'me'}}
|
|
||||||
* @param {Function} cb - callback called with (err, instance)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.findOne = function findOne(params, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'findOne');
|
|
||||||
};
|
|
||||||
|
|
||||||
setRemoting(DataModel.findOne, {
|
|
||||||
description: 'Find first instance of the model matched by filter from the data source',
|
|
||||||
accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'},
|
|
||||||
returns: {arg: 'data', type: 'object', root: true},
|
|
||||||
http: {verb: 'get', path: '/findOne'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy all matching records
|
|
||||||
* @param {Object} [where] An object that defines the criteria
|
|
||||||
* @param {Function} [cb] - callback called with (err)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.destroyAll = function destroyAll(where, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'destroyAll');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for `destroyAll`
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.remove = DataModel.destroyAll;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for `destroyAll`
|
|
||||||
*/
|
|
||||||
DataModel.deleteAll = DataModel.destroyAll;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy a record by id
|
|
||||||
* @param {*} id The id value
|
|
||||||
* @param {Function} cb - callback called with (err)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.destroyById = function deleteById(id, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'deleteById');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for deleteById
|
|
||||||
*/
|
|
||||||
DataModel.deleteById = DataModel.destroyById;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for deleteById
|
|
||||||
*/
|
|
||||||
DataModel.removeById = DataModel.destroyById;
|
|
||||||
|
|
||||||
// deleteById ~ remoting attributes
|
|
||||||
setRemoting(DataModel.deleteById, {
|
|
||||||
description: 'Delete a model instance by id from the data source',
|
|
||||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
|
||||||
http: {verb: 'del', path: '/:id'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return count of matched records
|
|
||||||
*
|
|
||||||
* @param {Object} where - search conditions (optional)
|
|
||||||
* @param {Function} cb - callback, called with (err, count)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.count = function (where, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'count');
|
|
||||||
};
|
|
||||||
|
|
||||||
// count ~ remoting attributes
|
|
||||||
setRemoting(DataModel.count, {
|
|
||||||
description: 'Count instances of the model matched by where from the data source',
|
|
||||||
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
|
|
||||||
returns: {arg: 'count', type: 'number'},
|
|
||||||
http: {verb: 'get', path: '/count'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save instance. When instance does not have an ID, create method instead.
|
|
||||||
* Triggers: validate, save, update or create.
|
|
||||||
*
|
|
||||||
* @options [options] Options
|
|
||||||
* @property {Boolean} validate Whether to validate.
|
|
||||||
* @property {Boolean} throws
|
|
||||||
* @callback {Function} callback Callback function.
|
|
||||||
* @param {Error} err Error object
|
|
||||||
* @param {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.save = function (options, callback) {
|
|
||||||
var inst = this;
|
|
||||||
var DataModel = inst.constructor;
|
|
||||||
|
|
||||||
if(typeof options === 'function') {
|
|
||||||
callback = options;
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// delegates directly to DataAccess
|
|
||||||
DataAccess.prototype.save.call(this, options, function(err, data) {
|
|
||||||
if(err) return callback(data);
|
|
||||||
var saved = new DataModel(data);
|
|
||||||
inst.setId(saved.getId());
|
|
||||||
callback(null, data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the data model is new.
|
|
||||||
* @returns {Boolean} True if data model is new.
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.isNewRecord = function () {
|
|
||||||
throwNotAttached(this.constructor.modelName, 'isNewRecord');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete object from persistence
|
|
||||||
*
|
|
||||||
* @triggers `destroy` hook (async) before and after destroying object
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.remove =
|
|
||||||
DataModel.prototype.delete =
|
|
||||||
DataModel.prototype.destroy = function (cb) {
|
|
||||||
throwNotAttached(this.constructor.modelName, 'destroy');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update single attribute.
|
|
||||||
* Equivalent to `updateAttributes({name: value}, cb)`
|
|
||||||
*
|
|
||||||
* @param {String} name - name of property
|
|
||||||
* @param {Mixed} value - value of property
|
|
||||||
* @param {Function} callback - callback called with (err, instance)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.updateAttribute = function updateAttribute(name, value, callback) {
|
|
||||||
throwNotAttached(this.constructor.modelName, 'updateAttribute');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update set of attributes
|
|
||||||
*
|
|
||||||
* Performs validation before updating
|
|
||||||
*
|
|
||||||
* @trigger `validation`, `save` and `update` hooks
|
|
||||||
* @param {Object} data - data to update
|
|
||||||
* @param {Function} callback - callback called with (err, instance)
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.updateAttributes = function updateAttributes(data, cb) {
|
|
||||||
throwNotAttached(this.modelName, 'updateAttributes');
|
|
||||||
};
|
|
||||||
|
|
||||||
// updateAttributes ~ remoting attributes
|
|
||||||
setRemoting(DataModel.prototype.updateAttributes, {
|
|
||||||
description: 'Update attributes for a model instance and persist it into the data source',
|
|
||||||
accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'},
|
|
||||||
returns: {arg: 'data', type: 'object', root: true},
|
|
||||||
http: {verb: 'put', path: '/'}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload object from persistence
|
|
||||||
*
|
|
||||||
* @requires `id` member of `object` to be able to call `find`
|
|
||||||
* @callback {Function} callback Callback function
|
|
||||||
* @param {Error} err
|
|
||||||
* @param {Object} instance
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.reload = function reload(callback) {
|
|
||||||
throwNotAttached(this.constructor.modelName, 'reload');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the corret `id` property for the `DataModel`. If a `Connector` defines
|
|
||||||
* a `setId` method it will be used. Otherwise the default lookup is used. You
|
|
||||||
* should override this method to handle complex ids.
|
|
||||||
*
|
|
||||||
* @param {*} val The `id` value. Will be converted to the type the id property
|
|
||||||
* specifies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.setId = function(val) {
|
|
||||||
var ds = this.getDataSource();
|
|
||||||
this[this.getIdName()] = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
DataModel.prototype.getId = function() {
|
|
||||||
var data = this.toObject();
|
|
||||||
if(!data) return;
|
|
||||||
return data[this.getIdName()];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the id property name of the constructor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.prototype.getIdName = function() {
|
|
||||||
return this.constructor.getIdName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the id property name
|
|
||||||
*/
|
|
||||||
|
|
||||||
DataModel.getIdName = function() {
|
|
||||||
var Model = this;
|
|
||||||
var ds = Model.getDataSource();
|
|
||||||
|
|
||||||
if(ds.idName) {
|
|
||||||
return ds.idName(Model.modelName);
|
|
||||||
} else {
|
|
||||||
return 'id';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,28 +2,99 @@
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
var registry = require('../registry');
|
var registry = require('../registry');
|
||||||
var compat = require('../compat');
|
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var SharedClass = require('strong-remoting').SharedClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The built in loopback.Model.
|
* The base class for **all models**.
|
||||||
*
|
*
|
||||||
|
* **Inheriting from `Model`**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* var properties = {...};
|
||||||
|
* var options = {...};
|
||||||
|
* var MyModel = loopback.Model.extend('MyModel', properties, options);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **Options**
|
||||||
|
*
|
||||||
|
* - `trackChanges` - If true, changes to the model will be tracked. **Required
|
||||||
|
* for replication.**
|
||||||
|
*
|
||||||
|
* **Events**
|
||||||
|
*
|
||||||
|
* #### Event: `changed`
|
||||||
|
*
|
||||||
|
* Emitted after a model has been successfully created, saved, or updated.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* MyModel.on('changed', function(inst) {
|
||||||
|
* console.log('model with id %s has been changed', inst.id);
|
||||||
|
* // => model with id 1 has been changed
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### Event: `deleted`
|
||||||
|
*
|
||||||
|
* Emitted after an individual model has been deleted.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* MyModel.on('deleted', function(inst) {
|
||||||
|
* console.log('model with id %s has been deleted', inst.id);
|
||||||
|
* // => model with id 1 has been deleted
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### Event: `deletedAll`
|
||||||
|
*
|
||||||
|
* Emitted after an individual model has been deleted.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* MyModel.on('deletedAll', function(where) {
|
||||||
|
* if(where) {
|
||||||
|
* console.log('all models where', where, 'have been deleted');
|
||||||
|
* // => all models where
|
||||||
|
* // => {price: {gt: 100}}
|
||||||
|
* // => have been deleted
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### Event: `attached`
|
||||||
|
*
|
||||||
|
* Emitted after a `Model` has been attached to an `app`.
|
||||||
|
*
|
||||||
|
* #### Event: `dataSourceAttached`
|
||||||
|
*
|
||||||
|
* Emitted after a `Model` has been attached to a `DataSource`.
|
||||||
|
*
|
||||||
* @class
|
* @class
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
|
* @property {String} modelName The name of the model
|
||||||
|
* @property {DataSource} dataSource
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Model = module.exports = registry.modelBuilder.define('Model');
|
var Model = module.exports = registry.modelBuilder.define('Model');
|
||||||
|
|
||||||
Model.shared = true;
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Called when a model is extended.
|
* Called when a model is extended.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Model.setup = function () {
|
Model.setup = function () {
|
||||||
var ModelCtor = this;
|
var ModelCtor = this;
|
||||||
|
var options = this.settings;
|
||||||
|
|
||||||
|
// create a sharedClass
|
||||||
|
var sharedClass = ModelCtor.sharedClass = new SharedClass(
|
||||||
|
ModelCtor.modelName,
|
||||||
|
ModelCtor,
|
||||||
|
options.remoting
|
||||||
|
);
|
||||||
|
|
||||||
|
// support remoting prototype methods
|
||||||
ModelCtor.sharedCtor = function (data, id, fn) {
|
ModelCtor.sharedCtor = function (data, id, fn) {
|
||||||
|
var ModelCtor = this;
|
||||||
|
|
||||||
if(typeof data === 'function') {
|
if(typeof data === 'function') {
|
||||||
fn = data;
|
fn = data;
|
||||||
data = null;
|
data = null;
|
||||||
|
@ -63,12 +134,24 @@ Model.setup = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var idDesc = ModelCtor.modelName + ' id';
|
||||||
|
ModelCtor.sharedCtor.accepts = [
|
||||||
|
{arg: 'id', type: 'any', 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 self = this;
|
var self = this;
|
||||||
if(this.app) {
|
if(this.app) {
|
||||||
var remotes = this.app.remotes();
|
var remotes = this.app.remotes();
|
||||||
var className = compat.getClassNameForRemoting(self);
|
var className = self.modelName;
|
||||||
remotes.before(className + '.' + name, function (ctx, next) {
|
remotes.before(className + '.' + name, function (ctx, next) {
|
||||||
fn(ctx, ctx.result, next);
|
fn(ctx, ctx.result, next);
|
||||||
});
|
});
|
||||||
|
@ -85,7 +168,7 @@ Model.setup = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
if(this.app) {
|
if(this.app) {
|
||||||
var remotes = this.app.remotes();
|
var remotes = this.app.remotes();
|
||||||
var className = compat.getClassNameForRemoting(self);
|
var className = self.modelName;
|
||||||
remotes.after(className + '.' + name, function (ctx, next) {
|
remotes.after(className + '.' + name, function (ctx, next) {
|
||||||
fn(ctx, ctx.result, next);
|
fn(ctx, ctx.result, next);
|
||||||
});
|
});
|
||||||
|
@ -97,19 +180,21 @@ Model.setup = function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map the prototype method to /:id with data in the body
|
// resolve relation functions
|
||||||
var idDesc = ModelCtor.modelName + ' id';
|
sharedClass.resolve(function resolver(define) {
|
||||||
ModelCtor.sharedCtor.accepts = [
|
var relations = ModelCtor.relations;
|
||||||
{arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc}
|
if(!relations) return;
|
||||||
// {arg: 'instance', type: 'object', http: {source: 'body'}}
|
// get the relations
|
||||||
];
|
for(var relationName in relations) {
|
||||||
|
var relation = relations[relationName];
|
||||||
|
if(relation.type === 'belongsTo') {
|
||||||
|
ModelCtor.belongsToRemoting(relationName, relation, define)
|
||||||
|
} else {
|
||||||
|
ModelCtor.scopeRemoting(relationName, relation, define);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ModelCtor.sharedCtor.http = [
|
|
||||||
{path: '/:id'}
|
|
||||||
];
|
|
||||||
|
|
||||||
ModelCtor.sharedCtor.returns = {root: true};
|
|
||||||
|
|
||||||
return ModelCtor;
|
return ModelCtor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,6 +225,7 @@ Model._ACL = function getACL(ACL) {
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object
|
||||||
* @param {Boolean} allowed True if the request is allowed; false otherwise.
|
* @param {Boolean} allowed True if the request is allowed; false otherwise.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Model.checkAccess = function(token, modelId, sharedMethod, callback) {
|
Model.checkAccess = function(token, modelId, sharedMethod, callback) {
|
||||||
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||||
token = token || ANONYMOUS;
|
token = token || ANONYMOUS;
|
||||||
|
@ -200,10 +286,8 @@ Model._getAccessTypeForMethod = function(method) {
|
||||||
return ACL.WRITE;
|
return ACL.WRITE;
|
||||||
case 'count':
|
case 'count':
|
||||||
return ACL.READ;
|
return ACL.READ;
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return ACL.EXECUTE;
|
return ACL.EXECUTE;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,48 +313,58 @@ Model.getApp = function(callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Model's `RemoteObjects`.
|
* Enable remote invocation for the method with the given name.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @param {String} name The name of the method.
|
||||||
* @param {Error} err
|
* ```js
|
||||||
* @param {RemoteObjects} remoteObjects
|
* // static method example (eg. Model.myMethod())
|
||||||
* @end
|
* Model.remoteMethod('myMethod');
|
||||||
|
* @param {Object} options The remoting options.
|
||||||
|
* See [loopback.remoteMethod()](http://docs.strongloop.com/display/DOC/Remote+methods+and+hooks#Remotemethodsandhooks-loopback.remoteMethod(fn,[options])) for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Model.remotes = function(callback) {
|
Model.remoteMethod = function(name, options) {
|
||||||
this.getApp(function(err, app) {
|
if(options.isStatic === undefined) {
|
||||||
callback(null, app.remotes());
|
options.isStatic = true;
|
||||||
});
|
}
|
||||||
|
this.sharedClass.defineMethod(name, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
Model.belongsToRemoting = function(relationName, relation, define) {
|
||||||
* Create a proxy function for invoking remote methods.
|
var fn = this.prototype[relationName];
|
||||||
*
|
define('__get__' + relationName, {
|
||||||
* @param {SharedMethod} sharedMethod
|
isStatic: false,
|
||||||
*/
|
http: {verb: 'get', path: '/' + relationName},
|
||||||
|
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
||||||
|
description: 'Fetches belongsTo relation ' + relationName,
|
||||||
|
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}
|
||||||
|
}, fn);
|
||||||
|
}
|
||||||
|
|
||||||
Model.createProxyMethod = function createProxyFunction(remoteMethod) {
|
Model.scopeRemoting = function(relationName, relation, define) {
|
||||||
var Model = this;
|
var toModelName = relation.modelTo.modelName;
|
||||||
var scope = remoteMethod.isStatic ? Model : Model.prototype;
|
|
||||||
var original = scope[remoteMethod.name];
|
define('__get__' + relationName, {
|
||||||
|
isStatic: false,
|
||||||
var fn = scope[remoteMethod.name] = function proxy() {
|
http: {verb: 'get', path: '/' + relationName},
|
||||||
var args = Array.prototype.slice.call(arguments);
|
accepts: {arg: 'filter', type: 'object'},
|
||||||
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
|
description: 'Queries ' + relationName + ' of ' + this.modelName + '.',
|
||||||
var callback;
|
returns: {arg: relationName, type: [toModelName], root: true}
|
||||||
if(lastArgIsFunc) {
|
});
|
||||||
callback = args.pop();
|
|
||||||
}
|
define('__create__' + relationName, {
|
||||||
|
isStatic: false,
|
||||||
Model.remotes(function(err, remotes) {
|
http: {verb: 'post', path: '/' + relationName},
|
||||||
remotes.invoke(remoteMethod.stringName, args, callback);
|
accepts: {arg: 'data', type: 'object', http: {source: 'body'}},
|
||||||
});
|
description: 'Creates a new instance in ' + relationName + ' of this model.',
|
||||||
}
|
returns: {arg: 'data', type: toModelName, root: true}
|
||||||
|
});
|
||||||
for(var key in original) {
|
|
||||||
fn[key] = original[key];
|
define('__delete__' + relationName, {
|
||||||
}
|
isStatic: false,
|
||||||
fn._delegate = true;
|
http: {verb: 'delete', path: '/' + relationName},
|
||||||
|
description: 'Deletes all ' + relationName + ' of this model.'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup the initial model
|
// setup the initial model
|
||||||
|
|
|
@ -0,0 +1,989 @@
|
||||||
|
/*!
|
||||||
|
* Module Dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Model = require('./model');
|
||||||
|
var runtime = require('../runtime');
|
||||||
|
var RemoteObjects = require('strong-remoting');
|
||||||
|
var assert = require('assert');
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends Model with basic query and CRUD support.
|
||||||
|
*
|
||||||
|
* **Change Event**
|
||||||
|
*
|
||||||
|
* Listen for model changes using the `change` event.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* MyPersistedModel.on('changed', function(obj) {
|
||||||
|
* console.log(obj) // => the changed model
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @class PersistedModel
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {Number} data.id The default id property
|
||||||
|
*/
|
||||||
|
|
||||||
|
var PersistedModel = module.exports = Model.extend('PersistedModel');
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Setup the `PersistedModel` constructor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.setup = function setupPersistedModel() {
|
||||||
|
// call Model.setup first
|
||||||
|
Model.setup.call(this);
|
||||||
|
|
||||||
|
var PersistedModel = this;
|
||||||
|
var typeName = this.modelName;
|
||||||
|
|
||||||
|
// setup a remoting type converter for this model
|
||||||
|
RemoteObjects.convert(typeName, function(val) {
|
||||||
|
return val ? new PersistedModel(val) : val;
|
||||||
|
});
|
||||||
|
|
||||||
|
// enable change tracking (usually for replication)
|
||||||
|
if(this.settings.trackChanges) {
|
||||||
|
PersistedModel._defineChangeModel();
|
||||||
|
PersistedModel.once('dataSourceAttached', function() {
|
||||||
|
PersistedModel.enableChangeTracking();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistedModel.setupRemoting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Throw an error telling the user that the method is not available and why.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function throwNotAttached(modelName, methodName) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot call ' + modelName + '.'+ methodName + '().'
|
||||||
|
+ ' The ' + methodName + ' method has not been setup.'
|
||||||
|
+ ' The PersistedModel has not been correctly attached to a DataSource!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Convert null callbacks to 404 error objects.
|
||||||
|
* @param {HttpContext} ctx
|
||||||
|
* @param {Function} cb
|
||||||
|
*/
|
||||||
|
|
||||||
|
function convertNullToNotFoundError(ctx, cb) {
|
||||||
|
if (ctx.result !== null) return cb();
|
||||||
|
|
||||||
|
var modelName = ctx.method.sharedClass.name;
|
||||||
|
var id = ctx.getArgByName('id');
|
||||||
|
var msg = 'Unknown "' + modelName + '" id "' + id + '".';
|
||||||
|
var error = new Error(msg);
|
||||||
|
error.statusCode = error.status = 404;
|
||||||
|
cb(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new instance of Model class, saved in database
|
||||||
|
*
|
||||||
|
* @param data [optional]
|
||||||
|
* @param callback(err, obj)
|
||||||
|
* callback called with arguments:
|
||||||
|
*
|
||||||
|
* - err (null or Error)
|
||||||
|
* - instance (null or Model)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.create = function (data, callback) {
|
||||||
|
throwNotAttached(this.modelName, 'create');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update or insert a model instance
|
||||||
|
* @param {Object} data The model instance data
|
||||||
|
* @param {Function} [callback] The callback function
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.upsert = PersistedModel.updateOrCreate = function upsert(data, callback) {
|
||||||
|
throwNotAttached(this.modelName, 'upsert');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find one record, same as `find`, limited by 1 and return object, not collection,
|
||||||
|
* if not found, create using data provided as second argument
|
||||||
|
*
|
||||||
|
* @param {Object} query - search conditions: {where: {test: 'me'}}.
|
||||||
|
* @param {Object} data - object to create.
|
||||||
|
* @param {Function} cb - callback called with (err, instance)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
|
||||||
|
throwNotAttached(this.modelName, 'findOrCreate');
|
||||||
|
};
|
||||||
|
|
||||||
|
PersistedModel.findOrCreate._delegate = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a model instance exists in database
|
||||||
|
*
|
||||||
|
* @param {id} id - identifier of object (primary key value)
|
||||||
|
* @param {Function} cb - callbacl called with (err, exists: Bool)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.exists = function exists(id, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'exists');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find object by id
|
||||||
|
*
|
||||||
|
* @param {*} id - primary key value
|
||||||
|
* @param {Function} cb - callback called with (err, instance)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.findById = function find(id, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'findById');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all instances of Model, matched by query
|
||||||
|
* make sure you have marked as `index: true` fields for filter or sort
|
||||||
|
*
|
||||||
|
* @param {Object} params (optional)
|
||||||
|
*
|
||||||
|
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
|
||||||
|
* - include: String, Object or Array. See PersistedModel.include documentation.
|
||||||
|
* - order: String
|
||||||
|
* - limit: Number
|
||||||
|
* - skip: Number
|
||||||
|
*
|
||||||
|
* @param {Function} callback (required) called with arguments:
|
||||||
|
*
|
||||||
|
* - err (null or Error)
|
||||||
|
* - Array of instances
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.find = function find(params, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'find');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find one record, same as `all`, limited by 1 and return object, not collection
|
||||||
|
*
|
||||||
|
* @param {Object} params - search conditions: {where: {test: 'me'}}
|
||||||
|
* @param {Function} cb - callback called with (err, instance)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.findOne = function findOne(params, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'findOne');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all matching records
|
||||||
|
* @param {Object} [where] An object that defines the criteria
|
||||||
|
* @param {Function} [cb] - callback called with (err)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.remove =
|
||||||
|
PersistedModel.deleteAll =
|
||||||
|
PersistedModel.destroyAll = function destroyAll(where, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'destroyAll');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update multiple instances that match the where clause
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
*```js
|
||||||
|
* Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) {
|
||||||
|
* ...
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Object} [where] Search conditions (optional)
|
||||||
|
* @param {Object} data Changes to be made
|
||||||
|
* @param {Function} cb Callback, called with (err, count)
|
||||||
|
*/
|
||||||
|
PersistedModel.update =
|
||||||
|
PersistedModel.updateAll = function updateAll(where, data, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'updateAll');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a record by id
|
||||||
|
* @param {*} id The id value
|
||||||
|
* @param {Function} cb - callback called with (err)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.removeById =
|
||||||
|
PersistedModel.deleteById =
|
||||||
|
PersistedModel.destroyById = function deleteById(id, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'deleteById');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return count of matched records
|
||||||
|
*
|
||||||
|
* @param {Object} where - search conditions (optional)
|
||||||
|
* @param {Function} cb - callback, called with (err, count)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.count = function (where, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'count');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save instance. When instance haven't id, create method called instead.
|
||||||
|
* Triggers: validate, save, update | create
|
||||||
|
* @param options {validate: true, throws: false} [optional]
|
||||||
|
* @param callback(err, obj)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.save = function (options, callback) {
|
||||||
|
var Model = this.constructor;
|
||||||
|
|
||||||
|
if (typeof options == 'function') {
|
||||||
|
callback = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
callback = callback || function () {
|
||||||
|
};
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
if (!('validate' in options)) {
|
||||||
|
options.validate = true;
|
||||||
|
}
|
||||||
|
if (!('throws' in options)) {
|
||||||
|
options.throws = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inst = this;
|
||||||
|
var data = inst.toObject(true);
|
||||||
|
var id = this.getId();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return Model.create(this, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate first
|
||||||
|
if (!options.validate) {
|
||||||
|
return save();
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.isValid(function (valid) {
|
||||||
|
if (valid) {
|
||||||
|
save();
|
||||||
|
} else {
|
||||||
|
var err = new ValidationError(inst);
|
||||||
|
// throws option is dangerous for async usage
|
||||||
|
if (options.throws) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
callback(err, inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// then save
|
||||||
|
function save() {
|
||||||
|
inst.trigger('save', function (saveDone) {
|
||||||
|
inst.trigger('update', function (updateDone) {
|
||||||
|
Model.upsert(inst, function(err) {
|
||||||
|
inst._initProperties(data);
|
||||||
|
updateDone.call(inst, function () {
|
||||||
|
saveDone.call(inst, function () {
|
||||||
|
callback(err, inst);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, data);
|
||||||
|
}, data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the data model is new.
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.isNewRecord = function () {
|
||||||
|
throwNotAttached(this.constructor.modelName, 'isNewRecord');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object from persistence
|
||||||
|
*
|
||||||
|
* @triggers `destroy` hook (async) before and after destroying object
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.remove =
|
||||||
|
PersistedModel.prototype.delete =
|
||||||
|
PersistedModel.prototype.destroy = function (cb) {
|
||||||
|
throwNotAttached(this.constructor.modelName, 'destroy');
|
||||||
|
};
|
||||||
|
|
||||||
|
PersistedModel.prototype.destroy._delegate = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update single attribute
|
||||||
|
*
|
||||||
|
* equals to `updateAttributes({name: value}, cb)
|
||||||
|
*
|
||||||
|
* @param {String} name - name of property
|
||||||
|
* @param {Mixed} value - value of property
|
||||||
|
* @param {Function} callback - callback called with (err, instance)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.updateAttribute = function updateAttribute(name, value, callback) {
|
||||||
|
throwNotAttached(this.constructor.modelName, 'updateAttribute');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update set of attributes
|
||||||
|
*
|
||||||
|
* this method performs validation before updating
|
||||||
|
*
|
||||||
|
* @trigger `validation`, `save` and `update` hooks
|
||||||
|
* @param {Object} data - data to update
|
||||||
|
* @param {Function} callback - callback called with (err, instance)
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'updateAttributes');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload object from persistence
|
||||||
|
*
|
||||||
|
* @requires `id` member of `object` to be able to call `find`
|
||||||
|
* @param {Function} callback - called with (err, instance) arguments
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.reload = function reload(callback) {
|
||||||
|
throwNotAttached(this.constructor.modelName, 'reload');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the correct `id` property for the `PersistedModel`. If a `Connector` defines
|
||||||
|
* a `setId` method it will be used. Otherwise the default lookup is used. You
|
||||||
|
* should override this method to handle complex ids.
|
||||||
|
*
|
||||||
|
* @param {*} val The `id` value. Will be converted to the type the id property
|
||||||
|
* specifies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.setId = function(val) {
|
||||||
|
var ds = this.getDataSource();
|
||||||
|
this[this.getIdName()] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the `id` value for the `PersistedModel`.
|
||||||
|
*
|
||||||
|
* @returns {*} The `id` value
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.getId = function() {
|
||||||
|
var data = this.toObject();
|
||||||
|
if(!data) return;
|
||||||
|
return data[this.getIdName()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the id property name of the constructor.
|
||||||
|
*
|
||||||
|
* @returns {String} The `id` property name
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.getIdName = function() {
|
||||||
|
return this.constructor.getIdName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the id property name of the constructor.
|
||||||
|
*
|
||||||
|
* @returns {String} The `id` property name
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.getIdName = function() {
|
||||||
|
var Model = this;
|
||||||
|
var ds = Model.getDataSource();
|
||||||
|
|
||||||
|
if(ds.idName) {
|
||||||
|
return ds.idName(Model.modelName);
|
||||||
|
} else {
|
||||||
|
return 'id';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistedModel.setupRemoting = function() {
|
||||||
|
var PersistedModel = this;
|
||||||
|
var typeName = PersistedModel.modelName;
|
||||||
|
var options = PersistedModel.settings;
|
||||||
|
|
||||||
|
function setRemoting(scope, name, options) {
|
||||||
|
var fn = scope[name];
|
||||||
|
fn._delegate = true;
|
||||||
|
options.isStatic = scope === PersistedModel;
|
||||||
|
PersistedModel.remoteMethod(name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'create', {
|
||||||
|
description: 'Create a new instance of the model and persist it into the data source',
|
||||||
|
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
|
||||||
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
|
http: {verb: 'post', path: '/'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'upsert', {
|
||||||
|
description: 'Update an existing model instance or insert a new one into the data source',
|
||||||
|
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
|
||||||
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
|
http: {verb: 'put', path: '/'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'exists', {
|
||||||
|
description: 'Check whether a model instance exists in the data source',
|
||||||
|
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
||||||
|
returns: {arg: 'exists', type: 'boolean'},
|
||||||
|
http: {verb: 'get', path: '/:id/exists'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'findById', {
|
||||||
|
description: 'Find a model instance by id from the data source',
|
||||||
|
accepts: {
|
||||||
|
arg: 'id', type: 'any', description: 'Model id', required: true,
|
||||||
|
http: {source: 'path'}
|
||||||
|
},
|
||||||
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
|
http: {verb: 'get', path: '/:id'},
|
||||||
|
rest: {after: convertNullToNotFoundError}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'find', {
|
||||||
|
description: 'Find all instances of the model matched by filter from the data source',
|
||||||
|
accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'},
|
||||||
|
returns: {arg: 'data', type: [typeName], root: true},
|
||||||
|
http: {verb: 'get', path: '/'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'findOne', {
|
||||||
|
description: 'Find first instance of the model matched by filter from the data source',
|
||||||
|
accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'},
|
||||||
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
|
http: {verb: 'get', path: '/findOne'},
|
||||||
|
rest: {after: convertNullToNotFoundError}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'destroyAll', {
|
||||||
|
description: 'Delete all matching records',
|
||||||
|
accepts: {arg: 'where', type: 'object', description: 'filter.where object'},
|
||||||
|
http: {verb: 'del', path: '/'},
|
||||||
|
shared: false
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'updateAll', {
|
||||||
|
description: 'Update instances of the model matched by where from the data source',
|
||||||
|
accepts: [
|
||||||
|
{arg: 'where', type: 'object', http: {source: 'query'},
|
||||||
|
description: 'Criteria to match model instances'},
|
||||||
|
{arg: 'data', type: 'object', http: {source: 'body'},
|
||||||
|
description: 'An object of model property name/value pairs'},
|
||||||
|
],
|
||||||
|
http: {verb: 'post', path: '/update'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'deleteById', {
|
||||||
|
description: 'Delete a model instance by id from the data source',
|
||||||
|
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true,
|
||||||
|
http: {source: 'path'}},
|
||||||
|
http: {verb: 'del', path: '/:id'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'count', {
|
||||||
|
description: 'Count instances of the model matched by where from the data source',
|
||||||
|
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
|
||||||
|
returns: {arg: 'count', type: 'number'},
|
||||||
|
http: {verb: 'get', path: '/count'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel.prototype, 'updateAttributes', {
|
||||||
|
description: 'Update attributes for a model instance and persist it into the data source',
|
||||||
|
accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'},
|
||||||
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
|
http: {verb: 'put', path: '/'}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(options.trackChanges) {
|
||||||
|
setRemoting(PersistedModel, 'diff', {
|
||||||
|
description: 'Get a set of deltas and conflicts since the given checkpoint',
|
||||||
|
accepts: [
|
||||||
|
{arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'},
|
||||||
|
{arg: 'remoteChanges', type: 'array', description: 'an array of change objects',
|
||||||
|
http: {source: 'body'}}
|
||||||
|
],
|
||||||
|
returns: {arg: 'result', type: 'object', root: true},
|
||||||
|
http: {verb: 'post', path: '/diff'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'changes', {
|
||||||
|
description: 'Get the changes to a model since a given checkpoint.'
|
||||||
|
+ 'Provide a filter object to reduce the number of results returned.',
|
||||||
|
accepts: [
|
||||||
|
{arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'},
|
||||||
|
{arg: 'filter', type: 'object', description: 'Only include changes that match this filter'}
|
||||||
|
],
|
||||||
|
returns: {arg: 'changes', type: 'array', root: true},
|
||||||
|
http: {verb: 'get', path: '/changes'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'checkpoint', {
|
||||||
|
description: 'Create a checkpoint.',
|
||||||
|
returns: {arg: 'checkpoint', type: 'object', root: true},
|
||||||
|
http: {verb: 'post', path: '/checkpoint'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'currentCheckpoint', {
|
||||||
|
description: 'Get the current checkpoint.',
|
||||||
|
returns: {arg: 'checkpoint', type: 'object', root: true},
|
||||||
|
http: {verb: 'get', path: '/checkpoint'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'createUpdates', {
|
||||||
|
description: 'Create an update list from a delta list',
|
||||||
|
accepts: {arg: 'deltas', type: 'array', http: {source: 'body'}},
|
||||||
|
returns: {arg: 'updates', type: 'array', root: true},
|
||||||
|
http: {verb: 'post', path: '/create-updates'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'bulkUpdate', {
|
||||||
|
description: 'Run multiple updates at once. Note: this is not atomic.',
|
||||||
|
accepts: {arg: 'updates', type: 'array'},
|
||||||
|
http: {verb: 'post', path: '/bulk-update'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'rectifyAllChanges', {
|
||||||
|
description: 'Rectify all Model changes.',
|
||||||
|
http: {verb: 'post', path: '/rectify-all'}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemoting(PersistedModel, 'rectifyChange', {
|
||||||
|
description: 'Tell loopback that a change to the model with the given id has occurred.',
|
||||||
|
accepts: {arg: 'id', type: 'any', http: {source: 'path'}},
|
||||||
|
http: {verb: 'post', path: '/:id/rectify-change'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a set of deltas and conflicts since the given checkpoint.
|
||||||
|
*
|
||||||
|
* See `Change.diff()` for details.
|
||||||
|
*
|
||||||
|
* @param {Number} since Find deltas since this checkpoint
|
||||||
|
* @param {Array} remoteChanges An array of change objects
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.diff = function(since, remoteChanges, callback) {
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
Change.diff(this.modelName, since, remoteChanges, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the changes to a model since a given checkpoint. Provide a filter object
|
||||||
|
* to reduce the number of results returned.
|
||||||
|
* @param {Number} since Only return changes since this checkpoint
|
||||||
|
* @param {Object} filter Only include changes that match this filter
|
||||||
|
* (same as `Model.find(filter, ...)`)
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Array} changes An array of `Change` objects
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.changes = function(since, filter, callback) {
|
||||||
|
if(typeof since === 'function') {
|
||||||
|
filter = {};
|
||||||
|
callback = since;
|
||||||
|
since = -1;
|
||||||
|
}
|
||||||
|
if(typeof filter === 'function') {
|
||||||
|
callback = filter;
|
||||||
|
since = -1;
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var idName = this.dataSource.idName(this.modelName);
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
var model = this;
|
||||||
|
|
||||||
|
filter = filter || {};
|
||||||
|
filter.fields = {};
|
||||||
|
filter.where = filter.where || {};
|
||||||
|
filter.fields[idName] = true;
|
||||||
|
|
||||||
|
// TODO(ritch) this whole thing could be optimized a bit more
|
||||||
|
Change.find({
|
||||||
|
checkpoint: {gt: since},
|
||||||
|
modelName: this.modelName
|
||||||
|
}, function(err, changes) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
var ids = changes.map(function(change) {
|
||||||
|
return change.getModelId();
|
||||||
|
});
|
||||||
|
filter.where[idName] = {inq: ids};
|
||||||
|
model.find(filter, function(err, models) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
var modelIds = models.map(function(m) {
|
||||||
|
return m[idName].toString();
|
||||||
|
});
|
||||||
|
callback(null, changes.filter(function(ch) {
|
||||||
|
if(ch.type() === Change.DELETE) return true;
|
||||||
|
return modelIds.indexOf(ch.modelId) > -1;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a checkpoint.
|
||||||
|
*
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.checkpoint = function(cb) {
|
||||||
|
var Checkpoint = this.getChangeModel().getCheckpointModel();
|
||||||
|
this.getSourceId(function(err, sourceId) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
Checkpoint.create({
|
||||||
|
sourceId: sourceId
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current checkpoint id.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Number} currentCheckpointId
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.currentCheckpoint = function(cb) {
|
||||||
|
var Checkpoint = this.getChangeModel().getCheckpointModel();
|
||||||
|
Checkpoint.current(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicate changes since the given checkpoint to the given target model.
|
||||||
|
*
|
||||||
|
* @param {Number} [since] Since this checkpoint
|
||||||
|
* @param {Model} targetModel Target this model class
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {Object} [options.filter] Replicate models that match this filter
|
||||||
|
* @callback {Function} [callback]
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Conflict[]} conflicts A list of changes that could not be replicated
|
||||||
|
* due to conflicts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.replicate = function(since, targetModel, options, callback) {
|
||||||
|
var lastArg = arguments[arguments.length - 1];
|
||||||
|
|
||||||
|
if(typeof lastArg === 'function' && arguments.length > 1) {
|
||||||
|
callback = lastArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof since === 'function' && since.modelName) {
|
||||||
|
targetModel = since;
|
||||||
|
since = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var sourceModel = this;
|
||||||
|
var diff;
|
||||||
|
var updates;
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
var TargetChange = targetModel.getChangeModel();
|
||||||
|
var changeTrackingEnabled = Change && TargetChange;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
changeTrackingEnabled,
|
||||||
|
'You must enable change tracking before replicating'
|
||||||
|
);
|
||||||
|
|
||||||
|
callback = callback || function defaultReplicationCallback(err) {
|
||||||
|
if(err) throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = [
|
||||||
|
getSourceChanges,
|
||||||
|
getDiffFromTarget,
|
||||||
|
createSourceUpdates,
|
||||||
|
bulkUpdate,
|
||||||
|
checkpoint
|
||||||
|
];
|
||||||
|
|
||||||
|
async.waterfall(tasks, done);
|
||||||
|
|
||||||
|
function getSourceChanges(cb) {
|
||||||
|
sourceModel.changes(since, options.filter, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiffFromTarget(sourceChanges, cb) {
|
||||||
|
targetModel.diff(since, sourceChanges, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSourceUpdates(_diff, cb) {
|
||||||
|
diff = _diff;
|
||||||
|
diff.conflicts = diff.conflicts || [];
|
||||||
|
if(diff && diff.deltas && diff.deltas.length) {
|
||||||
|
sourceModel.createUpdates(diff.deltas, cb);
|
||||||
|
} else {
|
||||||
|
// nothing to replicate
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bulkUpdate(updates, cb) {
|
||||||
|
targetModel.bulkUpdate(updates, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkpoint() {
|
||||||
|
var cb = arguments[arguments.length - 1];
|
||||||
|
sourceModel.checkpoint(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(err) {
|
||||||
|
if(err) return callback(err);
|
||||||
|
|
||||||
|
var conflicts = diff.conflicts.map(function(change) {
|
||||||
|
return new Change.Conflict(
|
||||||
|
change.modelId, sourceModel, targetModel
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(conflicts.length) {
|
||||||
|
sourceModel.emit('conflicts', conflicts);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback && callback(null, conflicts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an update list (for `Model.bulkUpdate()`) from a delta list
|
||||||
|
* (result of `Change.diff()`).
|
||||||
|
*
|
||||||
|
* @param {Array} deltas
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.createUpdates = function(deltas, cb) {
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
var updates = [];
|
||||||
|
var Model = this;
|
||||||
|
var tasks = [];
|
||||||
|
|
||||||
|
deltas.forEach(function(change) {
|
||||||
|
var change = new Change(change);
|
||||||
|
var type = change.type();
|
||||||
|
var update = {type: type, change: change};
|
||||||
|
switch(type) {
|
||||||
|
case Change.CREATE:
|
||||||
|
case Change.UPDATE:
|
||||||
|
tasks.push(function(cb) {
|
||||||
|
Model.findById(change.modelId, function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
if(!inst) {
|
||||||
|
console.error('missing data for change:', change);
|
||||||
|
return cb && cb(new Error('missing data for change: '
|
||||||
|
+ change.modelId));
|
||||||
|
}
|
||||||
|
if(inst.toObject) {
|
||||||
|
update.data = inst.toObject();
|
||||||
|
} else {
|
||||||
|
update.data = inst;
|
||||||
|
}
|
||||||
|
updates.push(update);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Change.DELETE:
|
||||||
|
updates.push(update);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async.parallel(tasks, function(err) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
cb(null, updates);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an update list.
|
||||||
|
*
|
||||||
|
* **Note: this is not atomic**
|
||||||
|
*
|
||||||
|
* @param {Array} updates An updates list (usually from Model.createUpdates())
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.bulkUpdate = function(updates, callback) {
|
||||||
|
var tasks = [];
|
||||||
|
var Model = this;
|
||||||
|
var idName = this.dataSource.idName(this.modelName);
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
|
||||||
|
updates.forEach(function(update) {
|
||||||
|
switch(update.type) {
|
||||||
|
case Change.UPDATE:
|
||||||
|
case Change.CREATE:
|
||||||
|
// var model = new Model(update.data);
|
||||||
|
// tasks.push(model.save.bind(model));
|
||||||
|
tasks.push(function(cb) {
|
||||||
|
var model = new Model(update.data);
|
||||||
|
model.save(cb);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Change.DELETE:
|
||||||
|
var data = {};
|
||||||
|
data[idName] = update.change.modelId;
|
||||||
|
var model = new Model(data);
|
||||||
|
tasks.push(model.destroy.bind(model));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async.parallel(tasks, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the `Change` model.
|
||||||
|
*
|
||||||
|
* @throws {Error} Throws an error if the change model is not correctly setup.
|
||||||
|
* @return {Change}
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.getChangeModel = function() {
|
||||||
|
var changeModel = this.Change;
|
||||||
|
var isSetup = changeModel && changeModel.dataSource;
|
||||||
|
|
||||||
|
assert(isSetup, 'Cannot get a setup Change model');
|
||||||
|
|
||||||
|
return changeModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the source identifier for this model / dataSource.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {String} sourceId
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.getSourceId = function(cb) {
|
||||||
|
var dataSource = this.dataSource;
|
||||||
|
if(!dataSource) {
|
||||||
|
this.once('dataSourceAttached', this.getSourceId.bind(this, cb));
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
dataSource.connector.name,
|
||||||
|
'Model.getSourceId: cannot get id without dataSource.connector.name'
|
||||||
|
);
|
||||||
|
var id = [dataSource.connector.name, this.modelName].join('-');
|
||||||
|
cb(null, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the tracking of changes made to the model. Usually for replication.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.enableChangeTracking = function() {
|
||||||
|
var Model = this;
|
||||||
|
var Change = this.Change || this._defineChangeModel();
|
||||||
|
var cleanupInterval = Model.settings.changeCleanupInterval || 30000;
|
||||||
|
|
||||||
|
assert(this.dataSource, 'Cannot enableChangeTracking(): ' + this.modelName
|
||||||
|
+ ' is not attached to a dataSource');
|
||||||
|
|
||||||
|
Change.attachTo(this.dataSource);
|
||||||
|
Change.getCheckpointModel().attachTo(this.dataSource);
|
||||||
|
|
||||||
|
Model.afterSave = function afterSave(next) {
|
||||||
|
Model.rectifyChange(this.getId(), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Model.afterDestroy = function afterDestroy(next) {
|
||||||
|
Model.rectifyChange(this.getId(), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Model.on('deletedAll', cleanup);
|
||||||
|
|
||||||
|
if(runtime.isServer) {
|
||||||
|
// initial cleanup
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
setInterval(cleanup, cleanupInterval);
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
Model.rectifyAllChanges(function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.error(Model.modelName + ' Change Cleanup Error:');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistedModel._defineChangeModel = function() {
|
||||||
|
var BaseChangeModel = require('./change');
|
||||||
|
return this.Change = BaseChangeModel.extend(this.modelName + '-change',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
trackModel: this
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistedModel.rectifyAllChanges = function(callback) {
|
||||||
|
this.getChangeModel().rectifyAll(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a change error. Override this method in a subclassing model to customize
|
||||||
|
* change error handling.
|
||||||
|
*
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.handleChangeError = function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.error(Model.modelName + ' Change Tracking Error:');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell loopback that a change to the model with the given id has occurred.
|
||||||
|
*
|
||||||
|
* @param {*} id The id of the model that has changed
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.rectifyChange = function(id, callback) {
|
||||||
|
var Change = this.getChangeModel();
|
||||||
|
Change.rectifyModelChanges(this.modelName, [id], callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistedModel.setup();
|
|
@ -2,8 +2,8 @@
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var loopback = require('../loopback')
|
var PersistedModel = require('../loopback').PersistedModel
|
||||||
, Model = loopback.Model
|
, loopback = require('../loopback')
|
||||||
, path = require('path')
|
, path = require('path')
|
||||||
, SALT_WORK_FACTOR = 10
|
, SALT_WORK_FACTOR = 10
|
||||||
, crypto = require('crypto')
|
, crypto = require('crypto')
|
||||||
|
@ -54,7 +54,7 @@ var options = {
|
||||||
principalType: ACL.ROLE,
|
principalType: ACL.ROLE,
|
||||||
principalId: Role.OWNER,
|
principalId: Role.OWNER,
|
||||||
permission: ACL.ALLOW,
|
permission: ACL.ALLOW,
|
||||||
property: 'removeById'
|
property: 'deleteById'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
principalType: ACL.ROLE,
|
principalType: ACL.ROLE,
|
||||||
|
@ -103,7 +103,7 @@ var options = {
|
||||||
*
|
*
|
||||||
* - DENY EVERYONE `*`
|
* - DENY EVERYONE `*`
|
||||||
* - ALLOW EVERYONE `create`
|
* - ALLOW EVERYONE `create`
|
||||||
* - ALLOW OWNER `removeById`
|
* - ALLOW OWNER `deleteById`
|
||||||
* - ALLOW EVERYONE `login`
|
* - ALLOW EVERYONE `login`
|
||||||
* - ALLOW EVERYONE `logout`
|
* - ALLOW EVERYONE `logout`
|
||||||
* - ALLOW EVERYONE `findById`
|
* - ALLOW EVERYONE `findById`
|
||||||
|
@ -113,7 +113,7 @@ var options = {
|
||||||
* @inherits {Model}
|
* @inherits {Model}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var User = module.exports = Model.extend('User', properties, options);
|
var User = module.exports = PersistedModel.extend('User', properties, options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create access token for the logged in user. This method can be overridden to
|
* Create access token for the logged in user. This method can be overridden to
|
||||||
|
@ -434,7 +434,7 @@ User.resetPassword = function(options, cb) {
|
||||||
|
|
||||||
User.setup = function () {
|
User.setup = function () {
|
||||||
// We need to call the base class's setup method
|
// We need to call the base class's setup method
|
||||||
Model.setup.call(this);
|
PersistedModel.setup.call(this);
|
||||||
var UserModel = this;
|
var UserModel = this;
|
||||||
|
|
||||||
// max ttl
|
// max ttl
|
||||||
|
@ -458,6 +458,7 @@ User.setup = function () {
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.login,
|
UserModel.login,
|
||||||
{
|
{
|
||||||
|
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' }, description:
|
{arg: 'include', type: 'string', http: {source: 'query' }, description:
|
||||||
|
@ -478,6 +479,7 @@ User.setup = function () {
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.logout,
|
UserModel.logout,
|
||||||
{
|
{
|
||||||
|
description: 'Logout a user with access token',
|
||||||
accepts: [
|
accepts: [
|
||||||
{arg: 'access_token', type: 'string', required: true, http: function(ctx) {
|
{arg: 'access_token', type: 'string', required: true, http: function(ctx) {
|
||||||
var req = ctx && ctx.req;
|
var req = ctx && ctx.req;
|
||||||
|
@ -497,6 +499,7 @@ User.setup = function () {
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.confirm,
|
UserModel.confirm,
|
||||||
{
|
{
|
||||||
|
description: 'Confirm a user registration with email verification token',
|
||||||
accepts: [
|
accepts: [
|
||||||
{arg: 'uid', type: 'string', required: true},
|
{arg: 'uid', type: 'string', required: true},
|
||||||
{arg: 'token', type: 'string', required: true},
|
{arg: 'token', type: 'string', required: true},
|
||||||
|
@ -509,6 +512,7 @@ User.setup = function () {
|
||||||
loopback.remoteMethod(
|
loopback.remoteMethod(
|
||||||
UserModel.resetPassword,
|
UserModel.resetPassword,
|
||||||
{
|
{
|
||||||
|
description: 'Reset password for a user with email',
|
||||||
accepts: [
|
accepts: [
|
||||||
{arg: 'options', type: 'object', required: true, http: {source: 'body'}}
|
{arg: 'options', type: 'object', required: true, http: {source: 'body'}}
|
||||||
],
|
],
|
||||||
|
@ -530,10 +534,12 @@ User.setup = function () {
|
||||||
UserModel.email = require('./email');
|
UserModel.email = require('./email');
|
||||||
UserModel.accessToken = require('./access-token');
|
UserModel.accessToken = require('./access-token');
|
||||||
|
|
||||||
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
// email validation regex
|
||||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
|
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
||||||
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||||
|
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
||||||
|
|
||||||
return UserModel;
|
return UserModel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ registry.createModel = function (name, properties, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseModel = BaseModel || this.Model;
|
BaseModel = BaseModel || this.PersistedModel;
|
||||||
|
|
||||||
var model = BaseModel.extend(name, properties, options);
|
var model = BaseModel.extend(name, properties, options);
|
||||||
|
|
||||||
|
@ -180,10 +180,26 @@ registry.configureModel = function(ModelCtor, config) {
|
||||||
* @param {String} modelName The model name
|
* @param {String} modelName The model name
|
||||||
* @returns {Model} The model class
|
* @returns {Model} The model class
|
||||||
*
|
*
|
||||||
|
* @header loopback.findModel(modelName)
|
||||||
|
*/
|
||||||
|
registry.findModel = function(modelName) {
|
||||||
|
return this.modelBuilder.models[modelName];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a model class by name from all models created by
|
||||||
|
* `loopback.createModel()`. Throw an error when no such model exists.
|
||||||
|
*
|
||||||
|
* @param {String} modelName The model name
|
||||||
|
* @returns {Model} The model class
|
||||||
|
*
|
||||||
* @header loopback.getModel(modelName)
|
* @header loopback.getModel(modelName)
|
||||||
*/
|
*/
|
||||||
registry.getModel = function(modelName) {
|
registry.getModel = function(modelName) {
|
||||||
return this.modelBuilder.models[modelName];
|
var model = this.findModel(modelName);
|
||||||
|
if (model) return model;
|
||||||
|
|
||||||
|
throw new Error('Model not found: ' + modelName);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -330,7 +346,19 @@ registry.DataSource = DataSource;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
registry.Model = require('./models/model');
|
registry.Model = require('./models/model');
|
||||||
registry.DataModel = require('./models/data-model');
|
registry.PersistedModel = require('./models/persisted-model');
|
||||||
|
|
||||||
|
// temporary alias to simplify migration of code based on <=2.0.0-beta3
|
||||||
|
Object.defineProperty(registry, 'DataModel', {
|
||||||
|
get: function() {
|
||||||
|
var stackLines = new Error().stack.split('\n');
|
||||||
|
console.warn('loopback.DataModel is deprecated, ' +
|
||||||
|
'use loopback.PersistedModel instead.');
|
||||||
|
// Log the location where loopback.DataModel was called
|
||||||
|
console.warn(stackLines[2]);
|
||||||
|
return this.PersistedModel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set the default model base class. This is done after the Model class is defined.
|
// Set the default model base class. This is done after the Model class is defined.
|
||||||
registry.modelBuilder.defaultModelBaseClass = registry.Model;
|
registry.modelBuilder.defaultModelBaseClass = registry.Model;
|
||||||
|
|
51
package.json
51
package.json
|
@ -26,48 +26,59 @@
|
||||||
"mobile",
|
"mobile",
|
||||||
"mBaaS"
|
"mBaaS"
|
||||||
],
|
],
|
||||||
"version": "1.10.0",
|
"version": "2.0.0-beta7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha -R spec"
|
"test": "grunt mocha-and-karma"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "~0.9.0",
|
"async": "~0.9.0",
|
||||||
"bcryptjs": "~2.0.1",
|
"body-parser": "~1.4.3",
|
||||||
"debug": "~1.0.3",
|
"canonical-json": "0.0.4",
|
||||||
"ejs": "~1.0.0",
|
"ejs": "~1.0.0",
|
||||||
"express": "3.x",
|
"express": "4.x",
|
||||||
|
"strong-remoting": "~2.0.0-beta5",
|
||||||
|
"bcryptjs": "~2.0.1",
|
||||||
|
"debug": "~1.0.4",
|
||||||
"inflection": "~1.3.8",
|
"inflection": "~1.3.8",
|
||||||
"nodemailer": "~0.7.1",
|
"nodemailer": "~1.0.1",
|
||||||
"strong-remoting": "~1.5.1",
|
"nodemailer-stub-transport": "~0.1.4",
|
||||||
"uid2": "0.0.3",
|
"uid2": "0.0.3",
|
||||||
"underscore": "~1.6.0",
|
"underscore": "~1.6.0",
|
||||||
"underscore.string": "~2.3.3"
|
"underscore.string": "~2.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"loopback-datasource-juggler": "^1.7.0"
|
"loopback-datasource-juggler": "~2.0.0-beta3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"loopback-datasource-juggler": "^1.7.0",
|
|
||||||
"mocha": "~1.20.1",
|
|
||||||
"strong-task-emitter": "0.0.x",
|
|
||||||
"supertest": "~0.13.0",
|
|
||||||
"chai": "~1.9.1",
|
|
||||||
"loopback-testing": "~0.2.0",
|
|
||||||
"browserify": "~4.2.1",
|
"browserify": "~4.2.1",
|
||||||
|
"chai": "~1.9.1",
|
||||||
|
"cookie-parser": "~1.3.2",
|
||||||
|
"errorhandler": "~1.1.1",
|
||||||
|
"es5-shim": "^4.0.0",
|
||||||
"grunt": "~0.4.5",
|
"grunt": "~0.4.5",
|
||||||
"grunt-browserify": "~2.1.3",
|
"grunt-browserify": "~2.1.3",
|
||||||
"grunt-contrib-uglify": "~0.5.0",
|
"grunt-cli": "^0.1.13",
|
||||||
"grunt-contrib-jshint": "~0.10.0",
|
"grunt-contrib-jshint": "~0.10.0",
|
||||||
|
"grunt-contrib-uglify": "~0.5.0",
|
||||||
"grunt-contrib-watch": "~0.6.1",
|
"grunt-contrib-watch": "~0.6.1",
|
||||||
"karma-script-launcher": "~0.1.0",
|
"grunt-karma": "~0.8.3",
|
||||||
|
"grunt-mocha-test": "^0.11.0",
|
||||||
|
"karma-browserify": "~0.2.1",
|
||||||
"karma-chrome-launcher": "~0.1.4",
|
"karma-chrome-launcher": "~0.1.4",
|
||||||
"karma-firefox-launcher": "~0.1.3",
|
"karma-firefox-launcher": "~0.1.3",
|
||||||
"karma-html2js-preprocessor": "~0.1.0",
|
"karma-html2js-preprocessor": "~0.1.0",
|
||||||
|
"karma-junit-reporter": "^0.2.2",
|
||||||
|
"karma-mocha": "^0.1.4",
|
||||||
"karma-phantomjs-launcher": "~0.1.4",
|
"karma-phantomjs-launcher": "~0.1.4",
|
||||||
"karma": "~0.12.17",
|
"karma-script-launcher": "~0.1.0",
|
||||||
"karma-browserify": "~0.2.1",
|
"loopback-boot": "^1.1.0",
|
||||||
"karma-mocha": "~0.1.4",
|
"loopback-datasource-juggler": "~2.0.0-beta3",
|
||||||
"grunt-karma": "~0.8.3"
|
"loopback-testing": "~0.2.0",
|
||||||
|
"mocha": "~1.20.1",
|
||||||
|
"serve-favicon": "~2.0.1",
|
||||||
|
"strong-task-emitter": "0.0.x",
|
||||||
|
"supertest": "~0.13.0",
|
||||||
|
"karma": "~0.12.17"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -200,7 +200,7 @@ function createTestApp(testToken, settings, done) {
|
||||||
principalId: "$everyone",
|
principalId: "$everyone",
|
||||||
accessType: ACL.ALL,
|
accessType: ACL.ALL,
|
||||||
permission: ACL.DENY,
|
permission: ACL.DENY,
|
||||||
property: 'removeById'
|
property: 'deleteById'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -209,7 +209,7 @@ function createTestApp(testToken, settings, done) {
|
||||||
modelOptions[key] = modelSettings[key];
|
modelOptions[key] = modelSettings[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
var TestModel = loopback.Model.extend('test', {}, modelOptions);
|
var TestModel = loopback.PersistedModel.extend('test', {}, modelOptions);
|
||||||
|
|
||||||
TestModel.attachTo(loopback.memory());
|
TestModel.attachTo(loopback.memory());
|
||||||
app.model(TestModel);
|
app.model(TestModel);
|
||||||
|
|
|
@ -21,7 +21,8 @@ before(function() {
|
||||||
|
|
||||||
describe('security scopes', function () {
|
describe('security scopes', function () {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
testModel = loopback.Model.extend('testModel');
|
var ds = this.ds = loopback.createDataSource({connector: loopback.Memory});
|
||||||
|
testModel = loopback.PersistedModel.extend('testModel');
|
||||||
ACL.attachTo(ds);
|
ACL.attachTo(ds);
|
||||||
Role.attachTo(ds);
|
Role.attachTo(ds);
|
||||||
RoleMapping.attachTo(ds);
|
RoleMapping.attachTo(ds);
|
||||||
|
|
359
test/app.test.js
359
test/app.test.js
|
@ -1,5 +1,10 @@
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||||
|
var loopback = require('../');
|
||||||
|
var PersistedModel = loopback.PersistedModel;
|
||||||
|
|
||||||
|
var describe = require('./util/describe');
|
||||||
|
var it = require('./util/it');
|
||||||
|
|
||||||
describe('app', function() {
|
describe('app', function() {
|
||||||
|
|
||||||
|
@ -11,22 +16,26 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Expose a `Model` to remote clients", function() {
|
it("Expose a `Model` to remote clients", function() {
|
||||||
var Color = db.createModel('color', {name: String});
|
var Color = PersistedModel.extend('color', {name: String});
|
||||||
app.model(Color);
|
app.model(Color);
|
||||||
|
Color.attachTo(db);
|
||||||
|
|
||||||
expect(app.models()).to.eql([Color]);
|
expect(app.models()).to.eql([Color]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses singlar name as app.remoteObjects() key', function() {
|
it('uses singlar name as app.remoteObjects() key', function() {
|
||||||
var Color = db.createModel('color', {name: String});
|
var Color = PersistedModel.extend('color', {name: String});
|
||||||
app.model(Color);
|
app.model(Color);
|
||||||
|
Color.attachTo(db);
|
||||||
expect(app.remoteObjects()).to.eql({ color: Color });
|
expect(app.remoteObjects()).to.eql({ color: Color });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses singular name as shared class name', function() {
|
it('uses singular name as shared class name', function() {
|
||||||
var Color = db.createModel('color', {name: String});
|
var Color = PersistedModel.extend('color', {name: String});
|
||||||
app.model(Color);
|
app.model(Color);
|
||||||
expect(app.remotes().exports).to.eql({ color: Color });
|
Color.attachTo(db);
|
||||||
|
var classes = app.remotes().classes().map(function(c) {return c.name});
|
||||||
|
expect(classes).to.contain('color');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registers existing models to app.models', function() {
|
it('registers existing models to app.models', function() {
|
||||||
|
@ -38,36 +47,16 @@ describe('app', function() {
|
||||||
expect(app.models.Color).to.equal(Color);
|
expect(app.models.Color).to.equal(Color);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('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) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var Color = db.createModel('color', {name: String});
|
var Color = PersistedModel.extend('color', {name: String});
|
||||||
app.model(Color);
|
app.model(Color);
|
||||||
|
Color.attachTo(db);
|
||||||
request(app).get('/colors').expect(200, done);
|
request(app).get('/colors').expect(200, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('in compat mode', function() {
|
|
||||||
before(function() {
|
|
||||||
loopback.compat.usePluralNamesForRemoting = true;
|
|
||||||
});
|
|
||||||
after(function() {
|
|
||||||
loopback.compat.usePluralNamesForRemoting = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses plural name as shared class name', function() {
|
|
||||||
var Color = db.createModel('color', {name: String});
|
|
||||||
app.model(Color);
|
|
||||||
expect(app.remotes().exports).to.eql({ colors: Color });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses plural name as app.remoteObjects() key', function() {
|
|
||||||
var Color = db.createModel('color', {name: String});
|
|
||||||
app.model(Color);
|
|
||||||
expect(app.remoteObjects()).to.eql({ colors: Color });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app.model(name, config)', function () {
|
describe('app.model(name, config)', function () {
|
||||||
|
@ -75,13 +64,8 @@ describe('app', function() {
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
app = loopback();
|
app = loopback();
|
||||||
app.boot({
|
app.dataSource('db', {
|
||||||
app: {port: 3000, host: '127.0.0.1'},
|
connector: 'memory'
|
||||||
dataSources: {
|
|
||||||
db: {
|
|
||||||
connector: 'memory'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -177,286 +161,7 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app.boot([options])', function () {
|
describe.onServer('listen()', function() {
|
||||||
beforeEach(function () {
|
|
||||||
app.boot({
|
|
||||||
app: {
|
|
||||||
port: 3000,
|
|
||||||
host: '127.0.0.1',
|
|
||||||
restApiRoot: '/rest-api',
|
|
||||||
foo: {bar: 'bat'},
|
|
||||||
baz: true
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
'foo-bar-bat-baz': {
|
|
||||||
options: {
|
|
||||||
plural: 'foo-bar-bat-bazzies'
|
|
||||||
},
|
|
||||||
dataSource: 'the-db'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataSources: {
|
|
||||||
'the-db': {
|
|
||||||
connector: 'memory'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have port setting', function () {
|
|
||||||
assert.equal(this.app.get('port'), 3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have host setting', function() {
|
|
||||||
assert.equal(this.app.get('host'), '127.0.0.1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have restApiRoot setting', function() {
|
|
||||||
assert.equal(this.app.get('restApiRoot'), '/rest-api');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have other settings', function () {
|
|
||||||
expect(this.app.get('foo')).to.eql({
|
|
||||||
bar: 'bat'
|
|
||||||
});
|
|
||||||
expect(this.app.get('baz')).to.eql(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('boot and models directories', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
var app = this.app = loopback();
|
|
||||||
app.boot(SIMPLE_APP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should run all modules in the boot directory', function () {
|
|
||||||
assert(process.loadedFooJS);
|
|
||||||
delete process.loadedFooJS;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should run all modules in the models directory', function () {
|
|
||||||
assert(process.loadedBarJS);
|
|
||||||
delete process.loadedBarJS;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PaaS and npm env variables', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
this.boot = function () {
|
|
||||||
var app = loopback();
|
|
||||||
app.boot({
|
|
||||||
app: {
|
|
||||||
port: undefined,
|
|
||||||
host: undefined
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be honored', function() {
|
|
||||||
var assertHonored = function (portKey, hostKey) {
|
|
||||||
process.env[hostKey] = randomPort();
|
|
||||||
process.env[portKey] = randomHost();
|
|
||||||
var app = this.boot();
|
|
||||||
assert.equal(app.get('port'), process.env[portKey]);
|
|
||||||
assert.equal(app.get('host'), process.env[hostKey]);
|
|
||||||
delete process.env[portKey];
|
|
||||||
delete process.env[hostKey];
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP');
|
|
||||||
assertHonored('npm_config_port', 'npm_config_host');
|
|
||||||
assertHonored('npm_package_config_port', 'npm_package_config_host');
|
|
||||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP');
|
|
||||||
assertHonored('PORT', 'HOST');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be honored in order', function() {
|
|
||||||
process.env.npm_config_host = randomHost();
|
|
||||||
process.env.OPENSHIFT_SLS_IP = randomHost();
|
|
||||||
process.env.OPENSHIFT_NODEJS_IP = randomHost();
|
|
||||||
process.env.HOST = randomHost();
|
|
||||||
process.env.npm_package_config_host = randomHost();
|
|
||||||
|
|
||||||
var app = this.boot();
|
|
||||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
|
||||||
|
|
||||||
delete process.env.npm_config_host;
|
|
||||||
delete process.env.OPENSHIFT_SLS_IP;
|
|
||||||
delete process.env.OPENSHIFT_NODEJS_IP;
|
|
||||||
delete process.env.HOST;
|
|
||||||
delete process.env.npm_package_config_host;
|
|
||||||
|
|
||||||
process.env.npm_config_port = randomPort();
|
|
||||||
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
|
||||||
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
|
||||||
process.env.PORT = randomPort();
|
|
||||||
process.env.npm_package_config_port = randomPort();
|
|
||||||
|
|
||||||
var app = this.boot();
|
|
||||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
|
||||||
assert.equal(app.get('port'), process.env.npm_config_port);
|
|
||||||
|
|
||||||
delete process.env.npm_config_port;
|
|
||||||
delete process.env.OPENSHIFT_SLS_PORT;
|
|
||||||
delete process.env.OPENSHIFT_NODEJS_PORT;
|
|
||||||
delete process.env.PORT;
|
|
||||||
delete process.env.npm_package_config_port;
|
|
||||||
});
|
|
||||||
|
|
||||||
function randomHost() {
|
|
||||||
return Math.random().toString().split('.')[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomPort() {
|
|
||||||
return Math.floor(Math.random() * 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should honor 0 for free port', function () {
|
|
||||||
var app = loopback();
|
|
||||||
app.boot({app: {port: 0}});
|
|
||||||
assert.equal(app.get('port'), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should default to port 3000', function () {
|
|
||||||
var app = loopback();
|
|
||||||
app.boot({app: {port: undefined}});
|
|
||||||
assert.equal(app.get('port'), 3000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Instantiate models', function () {
|
|
||||||
assert(app.models);
|
|
||||||
assert(app.models.FooBarBatBaz);
|
|
||||||
assert(app.models.fooBarBatBaz);
|
|
||||||
assertValidDataSource(app.models.FooBarBatBaz.dataSource);
|
|
||||||
assert.isFunc(app.models.FooBarBatBaz, 'find');
|
|
||||||
assert.isFunc(app.models.FooBarBatBaz, 'create');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Attach models to data sources', function () {
|
|
||||||
assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Instantiate data sources', function () {
|
|
||||||
assert(app.dataSources);
|
|
||||||
assert(app.dataSources.theDb);
|
|
||||||
assertValidDataSource(app.dataSources.theDb);
|
|
||||||
assert(app.dataSources.TheDb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('app.boot(appRootDir)', function () {
|
|
||||||
it('Load config files', function () {
|
|
||||||
var app = loopback();
|
|
||||||
|
|
||||||
app.boot(SIMPLE_APP);
|
|
||||||
|
|
||||||
assert(app.models.foo);
|
|
||||||
assert(app.models.Foo);
|
|
||||||
assert(app.models.Foo.dataSource);
|
|
||||||
assert.isFunc(app.models.Foo, 'find');
|
|
||||||
assert.isFunc(app.models.Foo, 'create');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('installMiddleware()', function() {
|
|
||||||
var app;
|
|
||||||
beforeEach(function() { app = loopback(); });
|
|
||||||
|
|
||||||
it('installs loopback.token', function(done) {
|
|
||||||
app.models.accessToken = loopback.AccessToken;
|
|
||||||
|
|
||||||
app.installMiddleware();
|
|
||||||
|
|
||||||
app.get('/', function(req, res) {
|
|
||||||
res.send({ accessTokenId: req.accessToken && req.accessToken.id });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.models.accessToken.create({}, function(err, token) {
|
|
||||||
if (err) done(err);
|
|
||||||
request(app).get('/')
|
|
||||||
.set('Authorization', token.id)
|
|
||||||
.expect(200, { accessTokenId: token.id })
|
|
||||||
.end(done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits "middleware:preprocessors" before handlers are installed',
|
|
||||||
function(done) {
|
|
||||||
app.on('middleware:preprocessors', function() {
|
|
||||||
this.use(function(req, res, next) {
|
|
||||||
req.preprocessed = true;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.installMiddleware();
|
|
||||||
|
|
||||||
app.get('/', function(req, res) {
|
|
||||||
res.send({ preprocessed: req.preprocessed });
|
|
||||||
});
|
|
||||||
|
|
||||||
request(app).get('/')
|
|
||||||
.expect(200, { preprocessed: true})
|
|
||||||
.end(done);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it('emits "middleware:handlers before installing express router',
|
|
||||||
function(done) {
|
|
||||||
app.on('middleware:handlers', function() {
|
|
||||||
this.use(function(req, res, next) {
|
|
||||||
res.send({ handler: 'middleware' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.installMiddleware();
|
|
||||||
|
|
||||||
app.get('/', function(req, res) {
|
|
||||||
res.send({ handler: 'router' });
|
|
||||||
});
|
|
||||||
|
|
||||||
request(app).get('/')
|
|
||||||
.expect(200, { handler: 'middleware' })
|
|
||||||
.end(done);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it('emits "middleware:error-handlers" after all request handlers',
|
|
||||||
function(done) {
|
|
||||||
var logs = [];
|
|
||||||
app.on('middleware:error-handlers', function() {
|
|
||||||
app.use(function(err, req, res, next) {
|
|
||||||
logs.push(req.url);
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.installMiddleware();
|
|
||||||
|
|
||||||
request(app).get('/not-found')
|
|
||||||
.expect(404)
|
|
||||||
.end(function(err, res) {
|
|
||||||
if (err) done(err);
|
|
||||||
expect(logs).to.eql(['/not-found']);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it('installs REST transport', function(done) {
|
|
||||||
app.model(loopback.Application);
|
|
||||||
app.set('restApiRoot', '/api');
|
|
||||||
app.installMiddleware();
|
|
||||||
|
|
||||||
request(app).get('/api/applications')
|
|
||||||
.expect(200, [])
|
|
||||||
.end(done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listen()', function() {
|
|
||||||
it('starts http server', function(done) {
|
it('starts http server', function(done) {
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
app.set('port', 0);
|
app.set('port', 0);
|
||||||
|
@ -528,7 +233,7 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('enableAuth', function() {
|
describe.onServer('enableAuth', function() {
|
||||||
it('should set app.isAuthEnabled to true', function() {
|
it('should set app.isAuthEnabled to true', function() {
|
||||||
expect(app.isAuthEnabled).to.not.equal(true);
|
expect(app.isAuthEnabled).to.not.equal(true);
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
|
@ -536,7 +241,7 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app.get("/", loopback.status())', function () {
|
describe.onServer('app.get("/", loopback.status())', function () {
|
||||||
it('should return the status of the application', function (done) {
|
it('should return the status of the application', function (done) {
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
app.get('/', loopback.status());
|
app.get('/', loopback.status());
|
||||||
|
@ -598,6 +303,28 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('app.settings', function() {
|
||||||
|
it('can be altered via `app.set(key, value)`', function() {
|
||||||
|
app.set('write-key', 'write-value');
|
||||||
|
expect(app.settings).to.have.property('write-key', 'write-value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be read via `app.get(key)`', function() {
|
||||||
|
app.settings['read-key'] = 'read-value';
|
||||||
|
expect(app.get('read-key')).to.equal('read-value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is unique per app instance', function() {
|
||||||
|
var app1 = loopback();
|
||||||
|
var app2 = loopback();
|
||||||
|
|
||||||
|
expect(app1.settings).to.not.equal(app2.settings);
|
||||||
|
|
||||||
|
app1.set('key', 'value');
|
||||||
|
expect(app2.get('key'), 'app2 value').to.equal(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('exposes loopback as a property', function() {
|
it('exposes loopback as a property', function() {
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
expect(app.loopback).to.equal(loopback);
|
expect(app.loopback).to.equal(loopback);
|
||||||
|
|
|
@ -0,0 +1,291 @@
|
||||||
|
var Change;
|
||||||
|
var TestModel;
|
||||||
|
|
||||||
|
describe('Change', function(){
|
||||||
|
beforeEach(function() {
|
||||||
|
var memory = loopback.createDataSource({
|
||||||
|
connector: loopback.Memory
|
||||||
|
});
|
||||||
|
TestModel = loopback.PersistedModel.extend('chtest', {}, {
|
||||||
|
trackChanges: true
|
||||||
|
});
|
||||||
|
this.modelName = TestModel.modelName;
|
||||||
|
TestModel.attachTo(memory);
|
||||||
|
Change = TestModel.getChangeModel();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var test = this;
|
||||||
|
test.data = {
|
||||||
|
foo: 'bar'
|
||||||
|
};
|
||||||
|
TestModel.create(test.data, function(err, model) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.model = model;
|
||||||
|
test.modelId = model.id;
|
||||||
|
test.revisionForModel = Change.revisionForInst(model);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.id', function () {
|
||||||
|
it('should be a hash of the modelName and modelId', function () {
|
||||||
|
var change = new Change({
|
||||||
|
rev: 'abc',
|
||||||
|
modelName: 'foo',
|
||||||
|
modelId: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
var hash = Change.hash([change.modelName, change.modelId].join('-'));
|
||||||
|
|
||||||
|
assert.equal(change.id, hash);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Change.rectifyModelChanges(modelName, modelIds, callback)', function () {
|
||||||
|
describe('using an existing untracked model', function () {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var test = this;
|
||||||
|
Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) {
|
||||||
|
if(err) return done(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an entry', function (done) {
|
||||||
|
var test = this;
|
||||||
|
Change.find(function(err, trackedChanges) {
|
||||||
|
assert.equal(trackedChanges[0].modelId, test.modelId.toString());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only create one change', function (done) {
|
||||||
|
Change.count(function(err, count) {
|
||||||
|
assert.equal(count, 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Change.findOrCreateChange(modelName, modelId, callback)', function () {
|
||||||
|
|
||||||
|
describe('when a change doesnt exist', function () {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var test = this;
|
||||||
|
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.result = result;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an entry', function (done) {
|
||||||
|
var test = this;
|
||||||
|
Change.findById(this.result.id, function(err, change) {
|
||||||
|
assert.equal(change.id, test.result.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a change does exist', function () {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var test = this;
|
||||||
|
Change.create({
|
||||||
|
modelName: test.modelName,
|
||||||
|
modelId: test.modelId
|
||||||
|
}, function(err, change) {
|
||||||
|
test.existingChange = change;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var test = this;
|
||||||
|
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.result = result;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find the entry', function (done) {
|
||||||
|
var test = this;
|
||||||
|
assert.equal(test.existingChange.id, test.result.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.rectify(callback)', function () {
|
||||||
|
it('should create a new change with the correct revision', function (done) {
|
||||||
|
var test = this;
|
||||||
|
var change = new Change({
|
||||||
|
modelName: this.modelName,
|
||||||
|
modelId: this.modelId
|
||||||
|
});
|
||||||
|
|
||||||
|
change.rectify(function(err, ch) {
|
||||||
|
assert.equal(ch.rev, test.revisionForModel);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.currentRevision(callback)', function () {
|
||||||
|
it('should get the correct revision', function (done) {
|
||||||
|
var test = this;
|
||||||
|
var change = new Change({
|
||||||
|
modelName: this.modelName,
|
||||||
|
modelId: this.modelId
|
||||||
|
});
|
||||||
|
|
||||||
|
change.currentRevision(function(err, rev) {
|
||||||
|
assert.equal(rev, test.revisionForModel);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Change.hash(str)', function () {
|
||||||
|
// todo(ritch) test other hashing algorithms
|
||||||
|
it('should hash the given string', function () {
|
||||||
|
var str = 'foo';
|
||||||
|
var hash = Change.hash(str);
|
||||||
|
assert(hash !== str);
|
||||||
|
assert(typeof hash === 'string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Change.revisionForInst(inst)', function () {
|
||||||
|
it('should return the same revision for the same data', function () {
|
||||||
|
var a = {
|
||||||
|
b: {
|
||||||
|
b: ['c', 'd'],
|
||||||
|
c: ['d', 'e']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var b = {
|
||||||
|
b: {
|
||||||
|
c: ['d', 'e'],
|
||||||
|
b: ['c', 'd']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var aRev = Change.revisionForInst(a);
|
||||||
|
var bRev = Change.revisionForInst(b);
|
||||||
|
assert.equal(aRev, bRev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.type()', function () {
|
||||||
|
it('CREATE', function () {
|
||||||
|
var change = new Change({
|
||||||
|
rev: this.revisionForModel
|
||||||
|
});
|
||||||
|
assert.equal(Change.CREATE, change.type());
|
||||||
|
});
|
||||||
|
it('UPDATE', function () {
|
||||||
|
var change = new Change({
|
||||||
|
rev: this.revisionForModel,
|
||||||
|
prev: this.revisionForModel
|
||||||
|
});
|
||||||
|
assert.equal(Change.UPDATE, change.type());
|
||||||
|
});
|
||||||
|
it('DELETE', function () {
|
||||||
|
var change = new Change({
|
||||||
|
prev: this.revisionForModel
|
||||||
|
});
|
||||||
|
assert.equal(Change.DELETE, change.type());
|
||||||
|
});
|
||||||
|
it('UNKNOWN', function () {
|
||||||
|
var change = new Change();
|
||||||
|
assert.equal(Change.UNKNOWN, change.type());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.getModelCtor()', function () {
|
||||||
|
it('should get the correct model class', function () {
|
||||||
|
var change = new Change({
|
||||||
|
modelName: this.modelName
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(change.getModelCtor(), TestModel);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.equals(otherChange)', function () {
|
||||||
|
it('should return true when the change is equal', function () {
|
||||||
|
var change = new Change({
|
||||||
|
rev: this.revisionForModel
|
||||||
|
});
|
||||||
|
|
||||||
|
var otherChange = new Change({
|
||||||
|
rev: this.revisionForModel
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(change.equals(otherChange), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when both changes are deletes', function () {
|
||||||
|
var REV = 'foo';
|
||||||
|
var change = new Change({
|
||||||
|
rev: null,
|
||||||
|
prev: REV,
|
||||||
|
});
|
||||||
|
|
||||||
|
var otherChange = new Change({
|
||||||
|
rev: undefined,
|
||||||
|
prev: REV
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(change.type(), Change.DELETE);
|
||||||
|
assert.equal(otherChange.type(), Change.DELETE);
|
||||||
|
|
||||||
|
assert.equal(change.equals(otherChange), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change.isBasedOn(otherChange)', function () {
|
||||||
|
it('should return true when the change is based on the other', function () {
|
||||||
|
var change = new Change({
|
||||||
|
prev: this.revisionForModel
|
||||||
|
});
|
||||||
|
|
||||||
|
var otherChange = new Change({
|
||||||
|
rev: this.revisionForModel
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(change.isBasedOn(otherChange), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Change.diff(modelName, since, remoteChanges, callback)', function () {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
Change.create([
|
||||||
|
{rev: 'foo', modelName: this.modelName, modelId: 9, checkpoint: 1},
|
||||||
|
{rev: 'bar', modelName: this.modelName, modelId: 10, checkpoint: 1},
|
||||||
|
{rev: 'bat', modelName: this.modelName, modelId: 11, checkpoint: 1},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return delta and conflict lists', 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, function(err, diff) {
|
||||||
|
assert.equal(diff.deltas.length, 1);
|
||||||
|
assert.equal(diff.conflicts.length, 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
var async = require('async');
|
||||||
|
var loopback = require('../');
|
||||||
|
|
||||||
|
// create a unique Checkpoint model
|
||||||
|
var Checkpoint = require('../lib/models/checkpoint').extend('TestCheckpoint');
|
||||||
|
Checkpoint.attachTo(loopback.memory());
|
||||||
|
|
||||||
|
describe('Checkpoint', function() {
|
||||||
|
describe('current()', function() {
|
||||||
|
it('returns the highest `seq` value', function(done) {
|
||||||
|
async.series([
|
||||||
|
Checkpoint.create.bind(Checkpoint),
|
||||||
|
Checkpoint.create.bind(Checkpoint),
|
||||||
|
function(next) {
|
||||||
|
Checkpoint.current(function(err, seq) {
|
||||||
|
if (err) next(err);
|
||||||
|
expect(seq).to.equal(2);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -35,12 +35,15 @@ describe('DataSource', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dataSource.operations()', function() {
|
describe.skip('PersistedModel Methods', function() {
|
||||||
it("List the enabled and disabled operations", function() {
|
it("List the enabled and disabled methods", function() {
|
||||||
|
var TestModel = loopback.PersistedModel.extend('TestPersistedModel');
|
||||||
|
TestModel.attachTo(loopback.memory());
|
||||||
|
|
||||||
// assert the defaults
|
// assert the defaults
|
||||||
// - true: the method should be remote enabled
|
// - true: the method should be remote enabled
|
||||||
// - false: the method should not be remote enabled
|
// - false: the method should not be remote enabled
|
||||||
// -
|
// -
|
||||||
existsAndShared('_forDB', false);
|
existsAndShared('_forDB', false);
|
||||||
existsAndShared('create', true);
|
existsAndShared('create', true);
|
||||||
existsAndShared('updateOrCreate', true);
|
existsAndShared('updateOrCreate', true);
|
||||||
|
@ -61,11 +64,15 @@ describe('DataSource', function() {
|
||||||
existsAndShared('destroyById', true);
|
existsAndShared('destroyById', true);
|
||||||
existsAndShared('destroy', false);
|
existsAndShared('destroy', false);
|
||||||
existsAndShared('updateAttributes', true);
|
existsAndShared('updateAttributes', true);
|
||||||
|
existsAndShared('updateAll', true);
|
||||||
existsAndShared('reload', false);
|
existsAndShared('reload', false);
|
||||||
|
|
||||||
function existsAndShared(name, isRemoteEnabled) {
|
function existsAndShared(Model, name, isRemoteEnabled, isProto) {
|
||||||
var op = memory.getOperation(name);
|
var scope = isProto ? Model.prototype : Model;
|
||||||
assert(op.remoteEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled');
|
var fn = scope[name];
|
||||||
|
var actuallyEnabled = Model.getRemoteMethod(name);
|
||||||
|
assert(fn, name + ' should be defined!');
|
||||||
|
assert(actuallyEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,12 +7,10 @@ var assert = require('assert');
|
||||||
describe('RemoteConnector', function() {
|
describe('RemoteConnector', function() {
|
||||||
before(function() {
|
before(function() {
|
||||||
// setup the remote connector
|
// setup the remote connector
|
||||||
var localApp = loopback();
|
|
||||||
var ds = loopback.createDataSource({
|
var ds = loopback.createDataSource({
|
||||||
url: 'http://localhost:3000/api',
|
url: 'http://localhost:3000/api',
|
||||||
connector: loopback.Remote
|
connector: loopback.Remote
|
||||||
});
|
});
|
||||||
localApp.model(TestModel);
|
|
||||||
TestModel.attachTo(ds);
|
TestModel.attachTo(ds);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,7 +30,7 @@ describe('RemoteConnector', function() {
|
||||||
});
|
});
|
||||||
m.save(function(err, data) {
|
m.save(function(err, data) {
|
||||||
if(err) return done(err);
|
if(err) return done(err);
|
||||||
assert(m.id);
|
assert(data.foo === 'bar');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
var path = require('path');
|
||||||
|
var loopback = require('../../');
|
||||||
|
var models = require('../fixtures/e2e/models');
|
||||||
|
var TestModel = models.TestModel;
|
||||||
|
var LocalTestModel = TestModel.extend('LocalTestModel', {}, {
|
||||||
|
trackChanges: true
|
||||||
|
});
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
describe('Replication', function() {
|
||||||
|
before(function() {
|
||||||
|
// setup the remote connector
|
||||||
|
var ds = loopback.createDataSource({
|
||||||
|
url: 'http://localhost:3000/api',
|
||||||
|
connector: loopback.Remote
|
||||||
|
});
|
||||||
|
TestModel.attachTo(ds);
|
||||||
|
var memory = loopback.memory();
|
||||||
|
LocalTestModel.attachTo(memory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replicate local data to the remote', function (done) {
|
||||||
|
var RANDOM = Math.random();
|
||||||
|
|
||||||
|
LocalTestModel.create({
|
||||||
|
n: RANDOM
|
||||||
|
}, function(err, created) {
|
||||||
|
LocalTestModel.replicate(0, TestModel, function() {
|
||||||
|
if(err) return done(err);
|
||||||
|
TestModel.findOne({n: RANDOM}, function(err, found) {
|
||||||
|
assert.equal(created.id, found.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -24,7 +24,8 @@ describe('Email and SMTP', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
MyEmail.send(options, function(err, mail) {
|
MyEmail.send(options, function(err, mail) {
|
||||||
assert(mail.message);
|
assert(!err);
|
||||||
|
assert(mail.response);
|
||||||
assert(mail.envelope);
|
assert(mail.envelope);
|
||||||
assert(mail.messageId);
|
assert(mail.messageId);
|
||||||
done(err);
|
done(err);
|
||||||
|
@ -41,7 +42,7 @@ describe('Email and SMTP', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
message.send(function (err, mail) {
|
message.send(function (err, mail) {
|
||||||
assert(mail.message);
|
assert(mail.response);
|
||||||
assert(mail.envelope);
|
assert(mail.envelope);
|
||||||
assert(mail.messageId);
|
assert(mail.messageId);
|
||||||
done(err);
|
done(err);
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
var loopback = require('../../../');
|
var loopback = require('../../../');
|
||||||
|
var boot = require('loopback-boot');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var app = module.exports = loopback();
|
var app = module.exports = loopback();
|
||||||
|
|
||||||
app.boot(__dirname);
|
boot(app, __dirname);
|
||||||
|
|
||||||
var apiPath = '/api';
|
var apiPath = '/api';
|
||||||
app.use(loopback.cookieParser('secret'));
|
app.use(loopback.cookieParser('secret'));
|
||||||
app.use(loopback.token({model: app.models.accessToken}));
|
app.use(loopback.token({model: app.models.accessToken}));
|
||||||
app.use(apiPath, loopback.rest());
|
app.use(apiPath, loopback.rest());
|
||||||
app.use(app.router);
|
|
||||||
app.use(loopback.urlNotFound());
|
app.use(loopback.urlNotFound());
|
||||||
app.use(loopback.errorHandler());
|
app.use(loopback.errorHandler());
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
|
|
|
@ -123,7 +123,7 @@
|
||||||
"permission": "DENY",
|
"permission": "DENY",
|
||||||
"principalType": "ROLE",
|
"principalType": "ROLE",
|
||||||
"principalId": "$owner",
|
"principalId": "$owner",
|
||||||
"property": "removeById"
|
"property": "deleteById"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,12 +3,18 @@ var path = require('path');
|
||||||
var app = module.exports = loopback();
|
var app = module.exports = loopback();
|
||||||
var models = require('./models');
|
var models = require('./models');
|
||||||
var TestModel = models.TestModel;
|
var TestModel = models.TestModel;
|
||||||
|
// var explorer = require('loopback-explorer');
|
||||||
|
|
||||||
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
||||||
var apiPath = '/api';
|
var apiPath = '/api';
|
||||||
app.use(apiPath, loopback.rest());
|
app.use(apiPath, loopback.rest());
|
||||||
|
|
||||||
|
TestModel.attachTo(loopback.memory());
|
||||||
|
app.model(TestModel);
|
||||||
|
app.model(TestModel.getChangeModel());
|
||||||
|
|
||||||
|
// app.use('/explorer', explorer(app, {basePath: apiPath}));
|
||||||
|
|
||||||
app.use(loopback.static(path.join(__dirname, 'public')));
|
app.use(loopback.static(path.join(__dirname, 'public')));
|
||||||
app.use(loopback.urlNotFound());
|
app.use(loopback.urlNotFound());
|
||||||
app.use(loopback.errorHandler());
|
app.use(loopback.errorHandler());
|
||||||
app.model(TestModel);
|
|
||||||
TestModel.attachTo(loopback.memory());
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
var loopback = require('../../../');
|
var loopback = require('../../../');
|
||||||
var DataModel = loopback.DataModel;
|
var PersistedModel = loopback.PersistedModel;
|
||||||
|
|
||||||
exports.TestModel = DataModel.extend('TestModel');
|
exports.TestModel = PersistedModel.extend('TestModel', {}, {
|
||||||
|
trackChanges: true
|
||||||
|
});
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
var loopback = require('../../../');
|
var loopback = require('../../../');
|
||||||
|
var boot = require('loopback-boot');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var app = module.exports = loopback();
|
var app = module.exports = loopback();
|
||||||
|
|
||||||
app.boot(__dirname);
|
boot(app, __dirname);
|
||||||
app.use(loopback.favicon());
|
app.use(loopback.favicon());
|
||||||
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
||||||
var apiPath = '/api';
|
var apiPath = '/api';
|
||||||
|
|
|
@ -3,15 +3,20 @@ var loopback = require('../');
|
||||||
describe('hidden properties', function () {
|
describe('hidden properties', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
var app = this.app = loopback();
|
var app = this.app = loopback();
|
||||||
var Product = this.Product = app.model('product', {
|
var Product = this.Product = loopback.PersistedModel.extend('product',
|
||||||
options: {hidden: ['secret']},
|
{},
|
||||||
dataSource: loopback.memory()
|
{hidden: ['secret']}
|
||||||
});
|
);
|
||||||
var Category = this.Category = this.app.model('category', {
|
Product.attachTo(loopback.memory());
|
||||||
dataSource: loopback.memory()
|
|
||||||
});
|
var Category = this.Category = loopback.PersistedModel.extend('category');
|
||||||
|
Category.attachTo(loopback.memory());
|
||||||
Category.hasMany(Product);
|
Category.hasMany(Product);
|
||||||
|
|
||||||
|
app.model(Product);
|
||||||
|
app.model(Category);
|
||||||
app.use(loopback.rest());
|
app.use(loopback.rest());
|
||||||
|
|
||||||
Category.create({
|
Category.create({
|
||||||
name: 'my category'
|
name: 'my category'
|
||||||
}, function(err, category) {
|
}, function(err, category) {
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Karma configuration
|
||||||
|
// http://karma-runner.github.io/0.12/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function(config) {
|
||||||
|
config.set({
|
||||||
|
// enable / disable watching file and executing tests whenever any file changes
|
||||||
|
autoWatch: true,
|
||||||
|
|
||||||
|
// base path, that will be used to resolve files and exclude
|
||||||
|
basePath: '../',
|
||||||
|
|
||||||
|
// testing framework to use (jasmine/mocha/qunit/...)
|
||||||
|
frameworks: ['mocha', 'browserify'],
|
||||||
|
|
||||||
|
// list of files / patterns to load in the browser
|
||||||
|
files: [
|
||||||
|
'node_modules/es5-shim/es5-shim.js',
|
||||||
|
'test/support.js',
|
||||||
|
'test/model.test.js',
|
||||||
|
'test/geo-point.test.js',
|
||||||
|
'test/app.test.js'
|
||||||
|
],
|
||||||
|
|
||||||
|
// list of files / patterns to exclude
|
||||||
|
exclude: [
|
||||||
|
],
|
||||||
|
|
||||||
|
// test results reporter to use
|
||||||
|
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
|
||||||
|
reporters: ['dots'],
|
||||||
|
|
||||||
|
// web server port
|
||||||
|
port: 9876,
|
||||||
|
|
||||||
|
// cli runner port
|
||||||
|
runnerPort: 9100,
|
||||||
|
|
||||||
|
// Start these browsers, currently available:
|
||||||
|
// - Chrome
|
||||||
|
// - ChromeCanary
|
||||||
|
// - Firefox
|
||||||
|
// - Opera
|
||||||
|
// - Safari (only Mac)
|
||||||
|
// - PhantomJS
|
||||||
|
// - IE (only Windows)
|
||||||
|
browsers: [
|
||||||
|
'Chrome'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Which plugins to enable
|
||||||
|
plugins: [
|
||||||
|
'karma-browserify',
|
||||||
|
'karma-mocha',
|
||||||
|
'karma-phantomjs-launcher',
|
||||||
|
'karma-chrome-launcher',
|
||||||
|
'karma-junit-reporter'
|
||||||
|
],
|
||||||
|
|
||||||
|
// If browser does not capture in given timeout [ms], kill it
|
||||||
|
captureTimeout: 60000,
|
||||||
|
|
||||||
|
// Continuous Integration mode
|
||||||
|
// if true, it capture browsers, run tests and exit
|
||||||
|
singleRun: false,
|
||||||
|
|
||||||
|
colors: true,
|
||||||
|
|
||||||
|
// level of logging
|
||||||
|
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
|
||||||
|
// Uncomment the following lines if you are using grunt's server to run the tests
|
||||||
|
// proxies: {
|
||||||
|
// '/': 'http://localhost:9000/'
|
||||||
|
// },
|
||||||
|
// URL root prevent conflicts with the site root
|
||||||
|
// urlRoot: '_karma_'
|
||||||
|
|
||||||
|
// Browserify config (all optional)
|
||||||
|
browserify: {
|
||||||
|
// extensions: ['.coffee'],
|
||||||
|
ignore: [
|
||||||
|
'nodemailer',
|
||||||
|
'passport',
|
||||||
|
'passport-local',
|
||||||
|
'superagent',
|
||||||
|
'supertest'
|
||||||
|
],
|
||||||
|
// transform: ['coffeeify'],
|
||||||
|
debug: true,
|
||||||
|
// noParse: ['jquery'],
|
||||||
|
watch: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add browserify to preprocessors
|
||||||
|
preprocessors: {'test/*': ['browserify']}
|
||||||
|
});
|
||||||
|
};
|
|
@ -124,7 +124,7 @@ describe('loopback', function() {
|
||||||
});
|
});
|
||||||
assert(loopback.getModel('MyModel') === MyModel);
|
assert(loopback.getModel('MyModel') === MyModel);
|
||||||
assert(loopback.getModel('MyCustomModel') === MyCustomModel);
|
assert(loopback.getModel('MyCustomModel') === MyCustomModel);
|
||||||
assert(loopback.getModel('Invalid') === undefined);
|
assert(loopback.findModel('Invalid') === undefined);
|
||||||
});
|
});
|
||||||
it('should be able to get model by type', function () {
|
it('should be able to get model by type', function () {
|
||||||
var MyModel = loopback.createModel('MyModel', {}, {
|
var MyModel = loopback.createModel('MyModel', {}, {
|
||||||
|
@ -141,6 +141,11 @@ describe('loopback', function() {
|
||||||
assert(loopback.getModelByType(MyModel) === MyCustomModel);
|
assert(loopback.getModelByType(MyModel) === MyCustomModel);
|
||||||
assert(loopback.getModelByType(MyCustomModel) === MyCustomModel);
|
assert(loopback.getModelByType(MyCustomModel) === MyCustomModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw when the model does not exist', function() {
|
||||||
|
expect(function() { loopback.getModel(uniqueModelName); })
|
||||||
|
.to.throw(Error, new RegExp('Model not found: ' + uniqueModelName));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,85 +1,42 @@
|
||||||
require('./support');
|
var async = require('async');
|
||||||
var ACL = require('../').ACL;
|
|
||||||
var loopback = require('../');
|
var loopback = require('../');
|
||||||
|
var ACL = loopback.ACL;
|
||||||
|
var Change = loopback.Change;
|
||||||
|
var defineModelTestsWithDataSource = require('./util/model-tests');
|
||||||
|
var PersistedModel = loopback.PersistedModel;
|
||||||
|
|
||||||
describe('Model', function() {
|
var describe = require('./util/describe');
|
||||||
|
|
||||||
var User, memory;
|
describe('Model / PersistedModel', function() {
|
||||||
|
defineModelTestsWithDataSource({
|
||||||
beforeEach(function () {
|
dataSource: {
|
||||||
memory = loopback.createDataSource({connector: loopback.Memory});
|
connector: loopback.Memory
|
||||||
User = memory.createModel('user', {
|
}
|
||||||
'first': String,
|
|
||||||
'last': String,
|
|
||||||
'age': Number,
|
|
||||||
'password': String,
|
|
||||||
'gender': String,
|
|
||||||
'domain': String,
|
|
||||||
'email': String
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.validatesPresenceOf(properties...)', function() {
|
|
||||||
it("Require a model to include a property to be considered valid", function() {
|
|
||||||
User.validatesPresenceOf('first', 'last', 'age');
|
|
||||||
var joe = new User({first: 'joe'});
|
|
||||||
assert(joe.isValid() === false, 'model should not validate');
|
|
||||||
assert(joe.errors.last, 'should have a missing last error');
|
|
||||||
assert(joe.errors.age, 'should have a missing age error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.validatesLengthOf(property, options)', function() {
|
|
||||||
it("Require a property length to be within a specified range", function() {
|
|
||||||
User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}});
|
|
||||||
var joe = new User({password: '1234'});
|
|
||||||
assert(joe.isValid() === false, 'model should not be valid');
|
|
||||||
assert(joe.errors.password, 'should have password error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.validatesInclusionOf(property, options)', function() {
|
|
||||||
it("Require a value for `property` to be in the specified array", function() {
|
|
||||||
User.validatesInclusionOf('gender', {in: ['male', 'female']});
|
|
||||||
var foo = new User({gender: 'bar'});
|
|
||||||
assert(foo.isValid() === false, 'model should not be valid');
|
|
||||||
assert(foo.errors.gender, 'should have gender error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.validatesExclusionOf(property, options)', function() {
|
|
||||||
it("Require a value for `property` to not exist in the specified array", function() {
|
|
||||||
User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']});
|
|
||||||
var foo = new User({domain: 'www'});
|
|
||||||
var bar = new User({domain: 'billing'});
|
|
||||||
var bat = new User({domain: 'admin'});
|
|
||||||
assert(foo.isValid() === false);
|
|
||||||
assert(bar.isValid() === false);
|
|
||||||
assert(bat.isValid() === false);
|
|
||||||
assert(foo.errors.domain, 'model should have a domain error');
|
|
||||||
assert(bat.errors.domain, 'model should have a domain error');
|
|
||||||
assert(bat.errors.domain, 'model should have a domain error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.validatesNumericalityOf(property, options)', function() {
|
|
||||||
it("Require a value for `property` to be a specific type of `Number`", function() {
|
|
||||||
User.validatesNumericalityOf('age', {int: true});
|
|
||||||
var joe = new User({age: 10.2});
|
|
||||||
assert(joe.isValid() === false);
|
|
||||||
var bob = new User({age: 0});
|
|
||||||
assert(bob.isValid() === true);
|
|
||||||
assert(joe.errors.age, 'model should have an age error');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model.validatesUniquenessOf(property, options)', function() {
|
describe('Model.validatesUniquenessOf(property, options)', function() {
|
||||||
it("Ensure the value for `property` is unique", function(done) {
|
it("Ensure the value for `property` is unique", function(done) {
|
||||||
|
var User = PersistedModel.extend('user', {
|
||||||
|
'first': String,
|
||||||
|
'last': String,
|
||||||
|
'age': Number,
|
||||||
|
'password': String,
|
||||||
|
'gender': String,
|
||||||
|
'domain': String,
|
||||||
|
'email': String
|
||||||
|
});
|
||||||
|
|
||||||
|
var dataSource = loopback.createDataSource({
|
||||||
|
connector: loopback.Memory
|
||||||
|
});
|
||||||
|
|
||||||
|
User.attachTo(dataSource);
|
||||||
|
|
||||||
User.validatesUniquenessOf('email', {message: 'email is not unique'});
|
User.validatesUniquenessOf('email', {message: 'email is not unique'});
|
||||||
|
|
||||||
var joe = new User({email: 'joe@joe.com'});
|
var joe = new User({email: 'joe@joe.com'});
|
||||||
var joe2 = new User({email: 'joe@joe.com'});
|
var joe2 = new User({email: 'joe@joe.com'});
|
||||||
|
|
||||||
joe.save(function () {
|
joe.save(function () {
|
||||||
joe2.save(function (err) {
|
joe2.save(function (err) {
|
||||||
assert(err, 'should get a validation error');
|
assert(err, 'should get a validation error');
|
||||||
|
@ -91,125 +48,72 @@ describe('Model', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('myModel.isValid()', function() {
|
|
||||||
it("Validate the model instance", function() {
|
|
||||||
User.validatesNumericalityOf('age', {int: true});
|
|
||||||
var user = new User({first: 'joe', age: 'flarg'})
|
|
||||||
var valid = user.isValid();
|
|
||||||
assert(valid === false);
|
|
||||||
assert(user.errors.age, 'model should have age error');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Asynchronously validate the model', function(done) {
|
|
||||||
User.validatesNumericalityOf('age', {int: true});
|
|
||||||
var user = new User({first: 'joe', age: 'flarg'})
|
|
||||||
user.isValid(function (valid) {
|
|
||||||
assert(valid === false);
|
|
||||||
assert(user.errors.age, 'model should have age error');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.attachTo(dataSource)', function() {
|
describe('Model.attachTo(dataSource)', function() {
|
||||||
it("Attach a model to a [DataSource](#data-source)", function() {
|
it("Attach a model to a [DataSource](#data-source)", function() {
|
||||||
var MyModel = loopback.createModel('my-model', {name: String});
|
var MyModel = loopback.createModel('my-model', {name: String});
|
||||||
|
var dataSource = loopback.createDataSource({
|
||||||
|
connector: loopback.Memory
|
||||||
|
});
|
||||||
|
|
||||||
assert(MyModel.find === undefined, 'should not have data access methods');
|
MyModel.attachTo(dataSource);
|
||||||
|
|
||||||
MyModel.attachTo(memory);
|
MyModel.find(function(err, results) {
|
||||||
|
assert(results.length === 0, 'should have data access methods after attaching to a data source');
|
||||||
assert(typeof MyModel.find === 'function', 'should have data access methods after attaching to a data source');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.create([data], [callback])', function() {
|
|
||||||
it("Create an instance of Model with given data and save to the attached data source", function(done) {
|
|
||||||
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
|
|
||||||
assert(user instanceof User);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('model.save([options], [callback])', function() {
|
describe.onServer('Remote Methods', function(){
|
||||||
it("Save an instance of a Model to the attached data source", function(done) {
|
|
||||||
var joe = new User({first: 'Joe', last: 'Bob'});
|
var User;
|
||||||
joe.save(function(err, user) {
|
var dataSource;
|
||||||
assert(user.id);
|
var app;
|
||||||
assert(!err);
|
|
||||||
assert(!user.errors);
|
beforeEach(function () {
|
||||||
done();
|
User = PersistedModel.extend('user', {
|
||||||
});
|
'first': String,
|
||||||
|
'last': String,
|
||||||
|
'age': Number,
|
||||||
|
'password': String,
|
||||||
|
'gender': String,
|
||||||
|
'domain': String,
|
||||||
|
'email': String
|
||||||
|
}, {
|
||||||
|
trackChanges: true
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('model.updateAttributes(data, [callback])', function() {
|
dataSource = loopback.createDataSource({
|
||||||
it("Save specified attributes to the attached data source", function(done) {
|
connector: loopback.Memory
|
||||||
User.create({first: 'joe', age: 100}, function (err, user) {
|
|
||||||
assert(!err);
|
|
||||||
assert.equal(user.first, 'joe');
|
|
||||||
|
|
||||||
user.updateAttributes({
|
|
||||||
first: 'updatedFirst',
|
|
||||||
last: 'updatedLast'
|
|
||||||
}, function (err, updatedUser) {
|
|
||||||
assert(!err);
|
|
||||||
assert.equal(updatedUser.first, 'updatedFirst');
|
|
||||||
assert.equal(updatedUser.last, 'updatedLast');
|
|
||||||
assert.equal(updatedUser.age, 100);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.upsert(data, callback)', function() {
|
User.attachTo(dataSource);
|
||||||
it("Update when record with id=data.id found, insert otherwise", function(done) {
|
|
||||||
User.upsert({first: 'joe', id: 7}, function (err, user) {
|
|
||||||
assert(!err);
|
|
||||||
assert.equal(user.first, 'joe');
|
|
||||||
|
|
||||||
User.upsert({first: 'bob', id: 7}, function (err, updatedUser) {
|
|
||||||
assert(!err);
|
|
||||||
assert.equal(updatedUser.first, 'bob');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('model.destroy([callback])', function() {
|
User.login = function (username, password, fn) {
|
||||||
it("Remove a model from the attached data source", function(done) {
|
if(username === 'foo' && password === 'bar') {
|
||||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
fn(null, 123);
|
||||||
User.findById(user.id, function (err, foundUser) {
|
} else {
|
||||||
assert.equal(user.id, foundUser.id);
|
throw new Error('bad username and password!');
|
||||||
foundUser.destroy(function () {
|
}
|
||||||
User.findById(user.id, function (err, notFound) {
|
}
|
||||||
assert(!err);
|
|
||||||
assert.equal(notFound, null);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.deleteById([callback])', function () {
|
loopback.remoteMethod(
|
||||||
it("Delete a model instance from the attached data source", function (done) {
|
User.login,
|
||||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
{
|
||||||
User.deleteById(user.id, function (err) {
|
accepts: [
|
||||||
User.findById(user.id, function (err, notFound) {
|
{arg: 'username', type: 'string', required: true},
|
||||||
assert(!err);
|
{arg: 'password', type: 'string', required: true}
|
||||||
assert.equal(notFound, null);
|
],
|
||||||
done();
|
returns: {arg: 'sessionId', type: 'any', root: true},
|
||||||
});
|
http: {path: '/sign-in', verb: 'get'}
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
app = loopback();
|
||||||
|
app.use(loopback.rest());
|
||||||
|
app.model(User);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Model.destroyAll(callback)', function() {
|
describe('Model.destroyAll(callback)', function() {
|
||||||
it("Delete all Model instances from data source", function(done) {
|
it("Delete all Model instances from data source", function(done) {
|
||||||
(new TaskEmitter())
|
(new TaskEmitter())
|
||||||
|
@ -220,7 +124,6 @@ describe('Model', function() {
|
||||||
.task(User, 'create', {first: 'suzy'})
|
.task(User, 'create', {first: 'suzy'})
|
||||||
.on('done', function () {
|
.on('done', function () {
|
||||||
User.count(function (err, count) {
|
User.count(function (err, count) {
|
||||||
assert.equal(count, 5);
|
|
||||||
User.destroyAll(function () {
|
User.destroyAll(function () {
|
||||||
User.count(function (err, count) {
|
User.count(function (err, count) {
|
||||||
assert.equal(count, 0);
|
assert.equal(count, 0);
|
||||||
|
@ -232,93 +135,97 @@ describe('Model', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model.findById(id, callback)', function() {
|
describe('Example Remote Method', function () {
|
||||||
it("Find an instance by id", function(done) {
|
it('Call the method using HTTP / REST', function(done) {
|
||||||
User.create({first: 'michael', last: 'jordan', id: 23}, function () {
|
request(app)
|
||||||
User.findById(23, function (err, user) {
|
.get('/users/sign-in?username=foo&password=bar')
|
||||||
assert.equal(user.id, 23);
|
.expect('Content-Type', /json/)
|
||||||
assert.equal(user.first, 'michael');
|
.expect(200)
|
||||||
assert.equal(user.last, 'jordan');
|
.end(function(err, res){
|
||||||
|
if(err) return done(err);
|
||||||
|
assert.equal(res.body, 123);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Converts null result of findById to 404 Not Found', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/users/not-found')
|
||||||
|
.expect(404)
|
||||||
|
.end(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model.count([query], callback)', function() {
|
describe('Model.beforeRemote(name, fn)', function(){
|
||||||
it("Query count of Model instances in data source", function(done) {
|
it('Run a function before a remote method is called by a client', function(done) {
|
||||||
(new TaskEmitter())
|
var hookCalled = false;
|
||||||
.task(User, 'create', {first: 'jill', age: 100})
|
|
||||||
.task(User, 'create', {first: 'bob', age: 200})
|
User.beforeRemote('create', function(ctx, user, next) {
|
||||||
.task(User, 'create', {first: 'jan'})
|
hookCalled = true;
|
||||||
.task(User, 'create', {first: 'sam'})
|
next();
|
||||||
.task(User, 'create', {first: 'suzy'})
|
});
|
||||||
.on('done', function () {
|
|
||||||
User.count({age: {gt: 99}}, function (err, count) {
|
// invoke save
|
||||||
assert.equal(count, 2);
|
request(app)
|
||||||
done();
|
.post('/users')
|
||||||
});
|
.send({data: {first: 'foo', last: 'bar'}})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if(err) return done(err);
|
||||||
|
assert(hookCalled, 'hook wasnt called');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.onServer('Remote Methods', function(){
|
describe('Model.afterRemote(name, fn)', function(){
|
||||||
|
it('Run a function after a remote method is called by a client', function(done) {
|
||||||
beforeEach(function () {
|
var beforeCalled = false;
|
||||||
User.login = function (username, password, fn) {
|
var afterCalled = false;
|
||||||
if(username === 'foo' && password === 'bar') {
|
|
||||||
fn(null, 123);
|
|
||||||
} else {
|
|
||||||
throw new Error('bad username and password!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loopback.remoteMethod(
|
|
||||||
User.login,
|
|
||||||
{
|
|
||||||
accepts: [
|
|
||||||
{arg: 'username', type: 'string', required: true},
|
|
||||||
{arg: 'password', type: 'string', required: true}
|
|
||||||
],
|
|
||||||
returns: {arg: 'sessionId', type: 'any', root: true},
|
|
||||||
http: {path: '/sign-in', verb: 'get'}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(loopback.rest());
|
User.beforeRemote('create', function(ctx, user, next) {
|
||||||
app.model(User);
|
assert(!afterCalled);
|
||||||
});
|
beforeCalled = true;
|
||||||
|
next();
|
||||||
describe('Example Remote Method', function () {
|
|
||||||
it('Call the method using HTTP / REST', function(done) {
|
|
||||||
request(app)
|
|
||||||
.get('/users/sign-in?username=foo&password=bar')
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
.end(function(err, res){
|
|
||||||
if(err) return done(err);
|
|
||||||
assert.equal(res.body, 123);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
User.afterRemote('create', function(ctx, user, next) {
|
||||||
|
assert(beforeCalled);
|
||||||
|
afterCalled = true;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// invoke save
|
||||||
|
request(app)
|
||||||
|
.post('/users')
|
||||||
|
.send({data: {first: 'foo', last: 'bar'}})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if(err) return done(err);
|
||||||
|
assert(beforeCalled, 'before hook was not called');
|
||||||
|
assert(afterCalled, 'after hook was not called');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Converts null result of findById to 404 Not Found', function(done) {
|
describe('Remote Method invoking context', function () {
|
||||||
request(app)
|
describe('ctx.req', function() {
|
||||||
.get('/users/not-found')
|
it("The express ServerRequest object", function(done) {
|
||||||
.expect(404)
|
|
||||||
.end(done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.beforeRemote(name, fn)', function(){
|
|
||||||
it('Run a function before a remote method is called by a client', function(done) {
|
|
||||||
var hookCalled = false;
|
var hookCalled = false;
|
||||||
|
|
||||||
User.beforeRemote('create', function(ctx, user, next) {
|
User.beforeRemote('create', function(ctx, user, next) {
|
||||||
hookCalled = true;
|
hookCalled = true;
|
||||||
|
assert(ctx.req);
|
||||||
|
assert(ctx.req.url);
|
||||||
|
assert(ctx.req.method);
|
||||||
|
assert(ctx.res);
|
||||||
|
assert(ctx.res.write);
|
||||||
|
assert(ctx.res.end);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// invoke save
|
// invoke save
|
||||||
request(app)
|
request(app)
|
||||||
.post('/users')
|
.post('/users')
|
||||||
|
@ -327,28 +234,27 @@ describe('Model', function() {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end(function(err, res) {
|
.end(function(err, res) {
|
||||||
if(err) return done(err);
|
if(err) return done(err);
|
||||||
assert(hookCalled, 'hook wasnt called');
|
assert(hookCalled);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model.afterRemote(name, fn)', function(){
|
describe('ctx.res', function() {
|
||||||
it('Run a function after a remote method is called by a client', function(done) {
|
it("The express ServerResponse object", function(done) {
|
||||||
var beforeCalled = false;
|
var hookCalled = false;
|
||||||
var afterCalled = false;
|
|
||||||
|
|
||||||
User.beforeRemote('create', function(ctx, user, next) {
|
User.beforeRemote('create', function(ctx, user, next) {
|
||||||
assert(!afterCalled);
|
hookCalled = true;
|
||||||
beforeCalled = true;
|
assert(ctx.req);
|
||||||
|
assert(ctx.req.url);
|
||||||
|
assert(ctx.req.method);
|
||||||
|
assert(ctx.res);
|
||||||
|
assert(ctx.res.write);
|
||||||
|
assert(ctx.res.end);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
User.afterRemote('create', function(ctx, user, next) {
|
|
||||||
assert(beforeCalled);
|
|
||||||
afterCalled = true;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// invoke save
|
// invoke save
|
||||||
request(app)
|
request(app)
|
||||||
.post('/users')
|
.post('/users')
|
||||||
|
@ -357,115 +263,17 @@ describe('Model', function() {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end(function(err, res) {
|
.end(function(err, res) {
|
||||||
if(err) return done(err);
|
if(err) return done(err);
|
||||||
assert(beforeCalled, 'before hook was not called');
|
assert(hookCalled);
|
||||||
assert(afterCalled, 'after hook was not called');
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
})
|
||||||
describe('Remote Method invoking context', function () {
|
|
||||||
// describe('ctx.user', function() {
|
|
||||||
// it("The remote user model calling the method remotely", function(done) {
|
|
||||||
// done(new Error('test not implemented'));
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
describe('ctx.req', function() {
|
|
||||||
it("The express ServerRequest object", function(done) {
|
|
||||||
var hookCalled = false;
|
|
||||||
|
|
||||||
User.beforeRemote('create', function(ctx, user, next) {
|
|
||||||
hookCalled = true;
|
|
||||||
assert(ctx.req);
|
|
||||||
assert(ctx.req.url);
|
|
||||||
assert(ctx.req.method);
|
|
||||||
assert(ctx.res);
|
|
||||||
assert(ctx.res.write);
|
|
||||||
assert(ctx.res.end);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// invoke save
|
|
||||||
request(app)
|
|
||||||
.post('/users')
|
|
||||||
.send({data: {first: 'foo', last: 'bar'}})
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
.end(function(err, res) {
|
|
||||||
if(err) return done(err);
|
|
||||||
assert(hookCalled);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ctx.res', function() {
|
|
||||||
it("The express ServerResponse object", function(done) {
|
|
||||||
var hookCalled = false;
|
|
||||||
|
|
||||||
User.beforeRemote('create', function(ctx, user, next) {
|
|
||||||
hookCalled = true;
|
|
||||||
assert(ctx.req);
|
|
||||||
assert(ctx.req.url);
|
|
||||||
assert(ctx.req.method);
|
|
||||||
assert(ctx.res);
|
|
||||||
assert(ctx.res.write);
|
|
||||||
assert(ctx.res.end);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// invoke save
|
|
||||||
request(app)
|
|
||||||
.post('/users')
|
|
||||||
.send({data: {first: 'foo', last: 'bar'}})
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
.end(function(err, res) {
|
|
||||||
if(err) return done(err);
|
|
||||||
assert(hookCalled);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('in compat mode', function() {
|
|
||||||
before(function() {
|
|
||||||
loopback.compat.usePluralNamesForRemoting = true;
|
|
||||||
});
|
|
||||||
after(function() {
|
|
||||||
loopback.compat.usePluralNamesForRemoting = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('correctly install before/after hooks', function(done) {
|
|
||||||
var hooksCalled = [];
|
|
||||||
|
|
||||||
User.beforeRemote('**', function(ctx, user, next) {
|
|
||||||
hooksCalled.push('beforeRemote');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
User.afterRemote('**', function(ctx, user, next) {
|
|
||||||
hooksCalled.push('afterRemote');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
request(app).get('/users')
|
|
||||||
.expect(200, function(err, res) {
|
|
||||||
if (err) return done(err);
|
|
||||||
expect(hooksCalled, 'hooks called')
|
|
||||||
.to.eql(['beforeRemote', 'afterRemote']);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Model.hasMany(Model)', function() {
|
describe('Model.hasMany(Model)', function() {
|
||||||
it("Define a one to many relationship", function(done) {
|
it("Define a one to many relationship", function(done) {
|
||||||
var Book = memory.createModel('book', {title: String, author: String});
|
var Book = dataSource.createModel('book', {title: String, author: String});
|
||||||
var Chapter = memory.createModel('chapter', {title: String});
|
var Chapter = dataSource.createModel('chapter', {title: String});
|
||||||
|
|
||||||
// by referencing model
|
// by referencing model
|
||||||
Book.hasMany(Chapter);
|
Book.hasMany(Chapter);
|
||||||
|
@ -523,7 +331,7 @@ describe('Model', function() {
|
||||||
|
|
||||||
describe('Model.extend()', function(){
|
describe('Model.extend()', function(){
|
||||||
it('Create a new model by extending an existing model', function() {
|
it('Create a new model by extending an existing model', function() {
|
||||||
var User = loopback.Model.extend('test-user', {
|
var User = loopback.PersistedModel.extend('test-user', {
|
||||||
email: String
|
email: String
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -557,33 +365,33 @@ describe('Model', function() {
|
||||||
|
|
||||||
describe('Model.extend() events', function() {
|
describe('Model.extend() events', function() {
|
||||||
it('create isolated emitters for subclasses', function() {
|
it('create isolated emitters for subclasses', function() {
|
||||||
var User1 = loopback.createModel('User1', {
|
var User1 = loopback.createModel('User1', {
|
||||||
'first': String,
|
'first': String,
|
||||||
'last': String
|
'last': String
|
||||||
});
|
});
|
||||||
|
|
||||||
var User2 = loopback.createModel('User2', {
|
var User2 = loopback.createModel('User2', {
|
||||||
'name': String
|
'name': String
|
||||||
});
|
});
|
||||||
|
|
||||||
var user1Triggered = false;
|
var user1Triggered = false;
|
||||||
User1.once('x', function(event) {
|
User1.once('x', function(event) {
|
||||||
user1Triggered = true;
|
user1Triggered = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
var user2Triggered = false;
|
var user2Triggered = false;
|
||||||
User2.once('x', function(event) {
|
User2.once('x', function(event) {
|
||||||
user2Triggered = true;
|
user2Triggered = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(User1.once !== User2.once);
|
assert(User1.once !== User2.once);
|
||||||
assert(User1.once !== loopback.Model.once);
|
assert(User1.once !== loopback.Model.once);
|
||||||
|
|
||||||
User1.emit('x', User1);
|
User1.emit('x', User1);
|
||||||
|
|
||||||
assert(user1Triggered);
|
assert(user1Triggered);
|
||||||
assert(!user2Triggered);
|
assert(!user2Triggered);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -615,6 +423,56 @@ describe('Model', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Model.getChangeModel()', function() {
|
||||||
|
it('Get the Change Model', function () {
|
||||||
|
var UserChange = User.getChangeModel();
|
||||||
|
var change = new UserChange();
|
||||||
|
assert(change instanceof Change);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.getSourceId(callback)', function() {
|
||||||
|
it('Get the Source Id', function (done) {
|
||||||
|
User.getSourceId(function(err, id) {
|
||||||
|
assert.equal('memory-user', id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.checkpoint(callback)', function() {
|
||||||
|
it('Create a checkpoint', function (done) {
|
||||||
|
var Checkpoint = User.getChangeModel().getCheckpointModel();
|
||||||
|
var tasks = [
|
||||||
|
getCurrentCheckpoint,
|
||||||
|
checkpoint
|
||||||
|
];
|
||||||
|
var result;
|
||||||
|
var current;
|
||||||
|
|
||||||
|
async.series(tasks, function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
|
||||||
|
assert.equal(result, current + 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCurrentCheckpoint(cb) {
|
||||||
|
Checkpoint.current(function(err, cp) {
|
||||||
|
current = cp;
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkpoint(cb) {
|
||||||
|
User.checkpoint(function(err, cp) {
|
||||||
|
result = cp.seq;
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Model._getACLModel()', function() {
|
describe('Model._getACLModel()', function() {
|
||||||
it('should return the subclass of ACL', function() {
|
it('should return the subclass of ACL', function() {
|
||||||
var Model = require('../').Model;
|
var Model = require('../').Model;
|
||||||
|
|
|
@ -378,7 +378,6 @@ describe('relations - integration', function () {
|
||||||
it.skip('allows to find related objects via where filter', function(done) {
|
it.skip('allows to find related objects via where filter', function(done) {
|
||||||
//TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94
|
//TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94
|
||||||
var expectedProduct = this.product;
|
var expectedProduct = this.product;
|
||||||
// Note: the URL format is not final
|
|
||||||
this.get('/api/products?filter[where][categoryId]=' + this.category.id)
|
this.get('/api/products?filter[where][categoryId]=' + this.category.id)
|
||||||
.expect(200, function(err, res) {
|
.expect(200, function(err, res) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
|
@ -1,42 +1,70 @@
|
||||||
var loopback = require('../');
|
var loopback = require('../');
|
||||||
|
var defineModelTestsWithDataSource = require('./util/model-tests');
|
||||||
|
|
||||||
describe('RemoteConnector', function() {
|
describe('RemoteConnector', function() {
|
||||||
|
var remoteApp;
|
||||||
|
var remote;
|
||||||
|
|
||||||
|
defineModelTestsWithDataSource({
|
||||||
|
beforeEach: function(done) {
|
||||||
|
var test = this;
|
||||||
|
remoteApp = loopback();
|
||||||
|
remoteApp.use(loopback.rest());
|
||||||
|
remoteApp.listen(0, function() {
|
||||||
|
test.dataSource = loopback.createDataSource({
|
||||||
|
host: remoteApp.get('host'),
|
||||||
|
port: remoteApp.get('port'),
|
||||||
|
connector: loopback.Remote
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDefine: function(Model) {
|
||||||
|
var RemoteModel = Model.extend(Model.modelName);
|
||||||
|
RemoteModel.attachTo(loopback.createDataSource({
|
||||||
|
connector: loopback.Memory
|
||||||
|
}));
|
||||||
|
remoteApp.model(RemoteModel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
var LocalModel = this.LocalModel = loopback.DataModel.extend('LocalModel');
|
var test = this;
|
||||||
var RemoteModel = loopback.DataModel.extend('LocalModel');
|
remoteApp = this.remoteApp = loopback();
|
||||||
var localApp = loopback();
|
|
||||||
var remoteApp = loopback();
|
|
||||||
localApp.model(LocalModel);
|
|
||||||
remoteApp.model(RemoteModel);
|
|
||||||
remoteApp.use(loopback.rest());
|
remoteApp.use(loopback.rest());
|
||||||
RemoteModel.attachTo(loopback.memory());
|
var ServerModel = this.ServerModel = loopback.PersistedModel.extend('TestModel');
|
||||||
|
|
||||||
|
remoteApp.model(ServerModel);
|
||||||
|
|
||||||
remoteApp.listen(0, function() {
|
remoteApp.listen(0, function() {
|
||||||
var ds = loopback.createDataSource({
|
test.remote = loopback.createDataSource({
|
||||||
host: remoteApp.get('host'),
|
host: remoteApp.get('host'),
|
||||||
port: remoteApp.get('port'),
|
port: remoteApp.get('port'),
|
||||||
connector: loopback.Remote
|
connector: loopback.Remote
|
||||||
});
|
});
|
||||||
|
|
||||||
LocalModel.attachTo(ds);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should alow methods to be called remotely', function (done) {
|
it('should support the save method', function (done) {
|
||||||
var data = {foo: 'bar'};
|
var calledServerCreate = false;
|
||||||
this.LocalModel.create(data, function(err, result) {
|
var RemoteModel = loopback.PersistedModel.extend('TestModel');
|
||||||
if(err) return done(err);
|
RemoteModel.attachTo(this.remote);
|
||||||
expect(result).to.deep.equal({id: 1, foo: 'bar'});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should alow instance methods to be called remotely', function (done) {
|
var ServerModel = this.ServerModel;
|
||||||
var data = {foo: 'bar'};
|
|
||||||
var m = new this.LocalModel(data);
|
ServerModel.create = function(data, cb) {
|
||||||
m.save(function(err, result) {
|
calledServerCreate = true;
|
||||||
if(err) return done(err);
|
data.id = 1;
|
||||||
expect(result).to.deep.equal({id: 2, foo: 'bar'});
|
cb(null, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerModel.setupRemoting();
|
||||||
|
|
||||||
|
var m = new RemoteModel({foo: 'bar'});
|
||||||
|
m.save(function(err, inst) {
|
||||||
|
assert(inst instanceof RemoteModel);
|
||||||
|
assert(calledServerCreate);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe('remoting - integration', function () {
|
||||||
it("should load remoting options", function () {
|
it("should load remoting options", function () {
|
||||||
var remotes = app.remotes();
|
var remotes = app.remotes();
|
||||||
assert.deepEqual(remotes.options, {"json": {"limit": "1kb", "strict": false},
|
assert.deepEqual(remotes.options, {"json": {"limit": "1kb", "strict": false},
|
||||||
"urlencoded": {"limit": "8kb"}});
|
"urlencoded": {"limit": "8kb", "extended": true}});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rest handler", function () {
|
it("rest handler", function () {
|
||||||
|
|
|
@ -0,0 +1,343 @@
|
||||||
|
var async = require('async');
|
||||||
|
var loopback = require('../');
|
||||||
|
var ACL = loopback.ACL;
|
||||||
|
var Change = loopback.Change;
|
||||||
|
var defineModelTestsWithDataSource = require('./util/model-tests');
|
||||||
|
var PersistedModel = loopback.PersistedModel;
|
||||||
|
|
||||||
|
describe('Replication / Change APIs', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
var test = this;
|
||||||
|
var dataSource = this.dataSource = loopback.createDataSource({
|
||||||
|
connector: loopback.Memory
|
||||||
|
});
|
||||||
|
var SourceModel = this.SourceModel = PersistedModel.extend('SourceModel', {}, {
|
||||||
|
trackChanges: true
|
||||||
|
});
|
||||||
|
SourceModel.attachTo(dataSource);
|
||||||
|
|
||||||
|
var TargetModel = this.TargetModel = PersistedModel.extend('TargetModel', {}, {
|
||||||
|
trackChanges: true
|
||||||
|
});
|
||||||
|
TargetModel.attachTo(dataSource);
|
||||||
|
|
||||||
|
this.createInitalData = function(cb) {
|
||||||
|
SourceModel.create({name: 'foo'}, function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
test.model = inst;
|
||||||
|
|
||||||
|
// give loopback a chance to register the change
|
||||||
|
// TODO(ritch) get rid of this...
|
||||||
|
setTimeout(function() {
|
||||||
|
SourceModel.replicate(TargetModel, cb);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.changes(since, filter, callback)', function() {
|
||||||
|
it('Get changes since the given checkpoint', function (done) {
|
||||||
|
var test = this;
|
||||||
|
this.SourceModel.create({name: 'foo'}, function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
setTimeout(function() {
|
||||||
|
test.SourceModel.changes(test.startingCheckpoint, {}, function(err, changes) {
|
||||||
|
assert.equal(changes.length, 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.replicate(since, targetModel, options, callback)', function() {
|
||||||
|
it('Replicate data using the target model', function (done) {
|
||||||
|
var test = this;
|
||||||
|
var options = {};
|
||||||
|
var sourceData;
|
||||||
|
var targetData;
|
||||||
|
|
||||||
|
this.SourceModel.create({name: 'foo'}, function(err) {
|
||||||
|
setTimeout(replicate, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
function replicate() {
|
||||||
|
test.SourceModel.replicate(test.startingCheckpoint, test.TargetModel,
|
||||||
|
options, function(err, conflicts) {
|
||||||
|
assert(conflicts.length === 0);
|
||||||
|
async.parallel([
|
||||||
|
function(cb) {
|
||||||
|
test.SourceModel.find(function(err, result) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
sourceData = result;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(cb) {
|
||||||
|
test.TargetModel.find(function(err, result) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
targetData = result;
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
|
||||||
|
assert.deepEqual(sourceData, targetData);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('conflict detection - both updated', function() {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var SourceModel = this.SourceModel;
|
||||||
|
var TargetModel = this.TargetModel;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
test.createInitalData(createConflict);
|
||||||
|
|
||||||
|
function createConflict(err, conflicts) {
|
||||||
|
async.parallel([
|
||||||
|
function(cb) {
|
||||||
|
SourceModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
inst.name = 'source update';
|
||||||
|
inst.save(cb);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(cb) {
|
||||||
|
TargetModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
inst.name = 'target update';
|
||||||
|
inst.save(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
SourceModel.replicate(TargetModel, function(err, conflicts) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.conflicts = conflicts;
|
||||||
|
test.conflict = conflicts[0];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should detect a single conflict', function() {
|
||||||
|
assert.equal(this.conflicts.length, 1);
|
||||||
|
assert(this.conflict);
|
||||||
|
});
|
||||||
|
it('type should be UPDATE', function(done) {
|
||||||
|
this.conflict.type(function(err, type) {
|
||||||
|
assert.equal(type, Change.UPDATE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('conflict.changes()', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.conflict.changes(function(err, sourceChange, targetChange) {
|
||||||
|
assert.equal(typeof sourceChange.id, 'string');
|
||||||
|
assert.equal(typeof targetChange.id, 'string');
|
||||||
|
assert.equal(test.model.getId(), sourceChange.getModelId());
|
||||||
|
assert.equal(sourceChange.type(), Change.UPDATE);
|
||||||
|
assert.equal(targetChange.type(), Change.UPDATE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('conflict.models()', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.conflict.models(function(err, source, target) {
|
||||||
|
assert.deepEqual(source.toJSON(), {
|
||||||
|
id: 1,
|
||||||
|
name: 'source update'
|
||||||
|
});
|
||||||
|
assert.deepEqual(target.toJSON(), {
|
||||||
|
id: 1,
|
||||||
|
name: 'target update'
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('conflict detection - source deleted', function() {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var SourceModel = this.SourceModel;
|
||||||
|
var TargetModel = this.TargetModel;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
test.createInitalData(createConflict);
|
||||||
|
|
||||||
|
function createConflict() {
|
||||||
|
async.parallel([
|
||||||
|
function(cb) {
|
||||||
|
SourceModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
test.model = inst;
|
||||||
|
inst.remove(cb);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(cb) {
|
||||||
|
TargetModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
inst.name = 'target update';
|
||||||
|
inst.save(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
SourceModel.replicate(TargetModel, function(err, conflicts) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.conflicts = conflicts;
|
||||||
|
test.conflict = conflicts[0];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should detect a single conflict', function() {
|
||||||
|
assert.equal(this.conflicts.length, 1);
|
||||||
|
assert(this.conflict);
|
||||||
|
});
|
||||||
|
it('type should be DELETE', function(done) {
|
||||||
|
this.conflict.type(function(err, type) {
|
||||||
|
assert.equal(type, Change.DELETE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('conflict.changes()', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.conflict.changes(function(err, sourceChange, targetChange) {
|
||||||
|
assert.equal(typeof sourceChange.id, 'string');
|
||||||
|
assert.equal(typeof targetChange.id, 'string');
|
||||||
|
assert.equal(test.model.getId(), sourceChange.getModelId());
|
||||||
|
assert.equal(sourceChange.type(), Change.DELETE);
|
||||||
|
assert.equal(targetChange.type(), Change.UPDATE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('conflict.models()', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.conflict.models(function(err, source, target) {
|
||||||
|
assert.equal(source, null);
|
||||||
|
assert.deepEqual(target.toJSON(), {
|
||||||
|
id: 1,
|
||||||
|
name: 'target update'
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('conflict detection - target deleted', function() {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var SourceModel = this.SourceModel;
|
||||||
|
var TargetModel = this.TargetModel;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
test.createInitalData(createConflict);
|
||||||
|
|
||||||
|
function createConflict() {
|
||||||
|
async.parallel([
|
||||||
|
function(cb) {
|
||||||
|
SourceModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
test.model = inst;
|
||||||
|
inst.name = 'source update';
|
||||||
|
inst.save(cb);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(cb) {
|
||||||
|
TargetModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
inst.remove(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
SourceModel.replicate(TargetModel, function(err, conflicts) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.conflicts = conflicts;
|
||||||
|
test.conflict = conflicts[0];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should detect a single conflict', function() {
|
||||||
|
assert.equal(this.conflicts.length, 1);
|
||||||
|
assert(this.conflict);
|
||||||
|
});
|
||||||
|
it('type should be DELETE', function(done) {
|
||||||
|
this.conflict.type(function(err, type) {
|
||||||
|
assert.equal(type, Change.DELETE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('conflict.changes()', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.conflict.changes(function(err, sourceChange, targetChange) {
|
||||||
|
assert.equal(typeof sourceChange.id, 'string');
|
||||||
|
assert.equal(typeof targetChange.id, 'string');
|
||||||
|
assert.equal(test.model.getId(), sourceChange.getModelId());
|
||||||
|
assert.equal(sourceChange.type(), Change.UPDATE);
|
||||||
|
assert.equal(targetChange.type(), Change.DELETE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('conflict.models()', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.conflict.models(function(err, source, target) {
|
||||||
|
assert.equal(target, null);
|
||||||
|
assert.deepEqual(source.toJSON(), {
|
||||||
|
id: 1,
|
||||||
|
name: 'source update'
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('conflict detection - both deleted', function() {
|
||||||
|
beforeEach(function(done) {
|
||||||
|
var SourceModel = this.SourceModel;
|
||||||
|
var TargetModel = this.TargetModel;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
test.createInitalData(createConflict);
|
||||||
|
|
||||||
|
function createConflict() {
|
||||||
|
async.parallel([
|
||||||
|
function(cb) {
|
||||||
|
SourceModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
test.model = inst;
|
||||||
|
inst.remove(cb);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(cb) {
|
||||||
|
TargetModel.findOne(function(err, inst) {
|
||||||
|
if(err) return cb(err);
|
||||||
|
inst.remove(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], function(err) {
|
||||||
|
if(err) return done(err);
|
||||||
|
SourceModel.replicate(TargetModel, function(err, conflicts) {
|
||||||
|
if(err) return done(err);
|
||||||
|
test.conflicts = conflicts;
|
||||||
|
test.conflict = conflicts[0];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should not detect a conflict', function() {
|
||||||
|
assert.equal(this.conflicts.length, 0);
|
||||||
|
assert(!this.conflict);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,6 +10,7 @@ GeoPoint = loopback.GeoPoint;
|
||||||
app = null;
|
app = null;
|
||||||
TaskEmitter = require('strong-task-emitter');
|
TaskEmitter = require('strong-task-emitter');
|
||||||
request = require('supertest');
|
request = require('supertest');
|
||||||
|
var RemoteObjects = require('strong-remoting');
|
||||||
|
|
||||||
// Speed up the password hashing algorithm
|
// Speed up the password hashing algorithm
|
||||||
// for tests using the built-in User model
|
// for tests using the built-in User model
|
||||||
|
@ -49,19 +50,3 @@ assert.isFunc = function (obj, name) {
|
||||||
assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist');
|
assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist');
|
||||||
assert(typeof obj[name] === 'function', name + ' is not a function');
|
assert(typeof obj[name] === 'function', name + ' is not a function');
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.onServer = function describeOnServer(name, fn) {
|
|
||||||
if (loopback.isServer) {
|
|
||||||
describe(name, fn);
|
|
||||||
} else {
|
|
||||||
describe.skip(name, fn);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
describe.inBrowser = function describeInBrowser(name, fn) {
|
|
||||||
if (loopback.isBrowser) {
|
|
||||||
describe(name, fn);
|
|
||||||
} else {
|
|
||||||
describe.skip(name, fn);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -109,6 +109,15 @@ describe('User', function(){
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Requires a unique username', function(done) {
|
||||||
|
User.create({email: 'a@b.com', username: 'abc', password: 'foobar'}, function () {
|
||||||
|
User.create({email: 'b@b.com', username: 'abc', password: 'batbaz'}, function (err) {
|
||||||
|
assert(err, 'should error because the username is not unique!');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Requires a password to login with basic auth', function(done) {
|
it('Requires a password to login with basic auth', function(done) {
|
||||||
User.create({email: 'b@c.com'}, function (err) {
|
User.create({email: 'b@c.com'}, function (err) {
|
||||||
|
@ -443,9 +452,9 @@ describe('User', function(){
|
||||||
|
|
||||||
user.verify(options, function (err, result) {
|
user.verify(options, function (err, result) {
|
||||||
assert(result.email);
|
assert(result.email);
|
||||||
assert(result.email.message);
|
assert(result.email.response);
|
||||||
assert(result.token);
|
assert(result.token);
|
||||||
assert(~result.email.message.indexOf('To: bar@bat.com'));
|
assert(~result.email.response.toString('utf-8').indexOf('To: bar@bat.com'));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
var loopback = require('../../');
|
||||||
|
|
||||||
|
module.exports = describe;
|
||||||
|
|
||||||
|
describe.onServer = function describeOnServer(name, fn) {
|
||||||
|
if (loopback.isServer) {
|
||||||
|
describe(name, fn);
|
||||||
|
} else {
|
||||||
|
describe.skip(name, fn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe.inBrowser = function describeInBrowser(name, fn) {
|
||||||
|
if (loopback.isBrowser) {
|
||||||
|
describe(name, fn);
|
||||||
|
} else {
|
||||||
|
describe.skip(name, fn);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
var loopback = require('../../');
|
||||||
|
|
||||||
|
module.exports = it;
|
||||||
|
|
||||||
|
it.onServer = function itOnServer(name, fn) {
|
||||||
|
if (loopback.isServer) {
|
||||||
|
it(name, fn);
|
||||||
|
} else {
|
||||||
|
it.skip(name, fn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it.inBrowser = function itInBrowser(name, fn) {
|
||||||
|
if (loopback.isBrowser) {
|
||||||
|
it(name, fn);
|
||||||
|
} else {
|
||||||
|
it.skip(name, fn);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,247 @@
|
||||||
|
var async = require('async');
|
||||||
|
var describe = require('./describe');
|
||||||
|
var loopback = require('../../');
|
||||||
|
var ACL = loopback.ACL;
|
||||||
|
var Change = loopback.Change;
|
||||||
|
var PersistedModel = loopback.PersistedModel;
|
||||||
|
var RemoteObjects = require('strong-remoting');
|
||||||
|
|
||||||
|
module.exports = function defineModelTestsWithDataSource(options) {
|
||||||
|
|
||||||
|
describe('Model Tests', function() {
|
||||||
|
|
||||||
|
var User, dataSource;
|
||||||
|
|
||||||
|
if(options.beforeEach) {
|
||||||
|
beforeEach(options.beforeEach);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
// setup a model / datasource
|
||||||
|
dataSource = this.dataSource || loopback.createDataSource(options.dataSource);
|
||||||
|
|
||||||
|
var extend = PersistedModel.extend;
|
||||||
|
|
||||||
|
// create model hook
|
||||||
|
PersistedModel.extend = function() {
|
||||||
|
var extendedModel = extend.apply(PersistedModel, arguments);
|
||||||
|
|
||||||
|
if(options.onDefine) {
|
||||||
|
options.onDefine.call(test, extendedModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return extendedModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
User = PersistedModel.extend('user', {
|
||||||
|
'first': String,
|
||||||
|
'last': String,
|
||||||
|
'age': Number,
|
||||||
|
'password': String,
|
||||||
|
'gender': String,
|
||||||
|
'domain': String,
|
||||||
|
'email': String
|
||||||
|
}, {
|
||||||
|
trackChanges: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// enable destroy all for testing
|
||||||
|
User.destroyAll.shared = true;
|
||||||
|
User.attachTo(dataSource);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.validatesPresenceOf(properties...)', function() {
|
||||||
|
it("Require a model to include a property to be considered valid", function() {
|
||||||
|
User.validatesPresenceOf('first', 'last', 'age');
|
||||||
|
var joe = new User({first: 'joe'});
|
||||||
|
assert(joe.isValid() === false, 'model should not validate');
|
||||||
|
assert(joe.errors.last, 'should have a missing last error');
|
||||||
|
assert(joe.errors.age, 'should have a missing age error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.validatesLengthOf(property, options)', function() {
|
||||||
|
it("Require a property length to be within a specified range", function() {
|
||||||
|
User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}});
|
||||||
|
var joe = new User({password: '1234'});
|
||||||
|
assert(joe.isValid() === false, 'model should not be valid');
|
||||||
|
assert(joe.errors.password, 'should have password error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.validatesInclusionOf(property, options)', function() {
|
||||||
|
it("Require a value for `property` to be in the specified array", function() {
|
||||||
|
User.validatesInclusionOf('gender', {in: ['male', 'female']});
|
||||||
|
var foo = new User({gender: 'bar'});
|
||||||
|
assert(foo.isValid() === false, 'model should not be valid');
|
||||||
|
assert(foo.errors.gender, 'should have gender error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.validatesExclusionOf(property, options)', function() {
|
||||||
|
it("Require a value for `property` to not exist in the specified array", function() {
|
||||||
|
User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']});
|
||||||
|
var foo = new User({domain: 'www'});
|
||||||
|
var bar = new User({domain: 'billing'});
|
||||||
|
var bat = new User({domain: 'admin'});
|
||||||
|
assert(foo.isValid() === false);
|
||||||
|
assert(bar.isValid() === false);
|
||||||
|
assert(bat.isValid() === false);
|
||||||
|
assert(foo.errors.domain, 'model should have a domain error');
|
||||||
|
assert(bat.errors.domain, 'model should have a domain error');
|
||||||
|
assert(bat.errors.domain, 'model should have a domain error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.validatesNumericalityOf(property, options)', function() {
|
||||||
|
it("Require a value for `property` to be a specific type of `Number`", function() {
|
||||||
|
User.validatesNumericalityOf('age', {int: true});
|
||||||
|
var joe = new User({age: 10.2});
|
||||||
|
assert(joe.isValid() === false);
|
||||||
|
var bob = new User({age: 0});
|
||||||
|
assert(bob.isValid() === true);
|
||||||
|
assert(joe.errors.age, 'model should have an age error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('myModel.isValid()', function() {
|
||||||
|
it("Validate the model instance", function() {
|
||||||
|
User.validatesNumericalityOf('age', {int: true});
|
||||||
|
var user = new User({first: 'joe', age: 'flarg'})
|
||||||
|
var valid = user.isValid();
|
||||||
|
assert(valid === false);
|
||||||
|
assert(user.errors.age, 'model should have age error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Asynchronously validate the model', function(done) {
|
||||||
|
User.validatesNumericalityOf('age', {int: true});
|
||||||
|
var user = new User({first: 'joe', age: 'flarg'});
|
||||||
|
user.isValid(function (valid) {
|
||||||
|
assert(valid === false);
|
||||||
|
assert(user.errors.age, 'model should have age error');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.create([data], [callback])', function() {
|
||||||
|
it("Create an instance of Model with given data and save to the attached data source", function(done) {
|
||||||
|
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
|
||||||
|
assert(user instanceof User);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('model.save([options], [callback])', function() {
|
||||||
|
it("Save an instance of a Model to the attached data source", function(done) {
|
||||||
|
var joe = new User({first: 'Joe', last: 'Bob'});
|
||||||
|
joe.save(function(err, user) {
|
||||||
|
assert(user.id);
|
||||||
|
assert(!err);
|
||||||
|
assert(!user.errors);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('model.updateAttributes(data, [callback])', function() {
|
||||||
|
it("Save specified attributes to the attached data source", function(done) {
|
||||||
|
User.create({first: 'joe', age: 100}, function (err, user) {
|
||||||
|
assert(!err);
|
||||||
|
assert.equal(user.first, 'joe');
|
||||||
|
|
||||||
|
user.updateAttributes({
|
||||||
|
first: 'updatedFirst',
|
||||||
|
last: 'updatedLast'
|
||||||
|
}, function (err, updatedUser) {
|
||||||
|
assert(!err);
|
||||||
|
assert.equal(updatedUser.first, 'updatedFirst');
|
||||||
|
assert.equal(updatedUser.last, 'updatedLast');
|
||||||
|
assert.equal(updatedUser.age, 100);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.upsert(data, callback)', function() {
|
||||||
|
it("Update when record with id=data.id found, insert otherwise", function(done) {
|
||||||
|
User.upsert({first: 'joe', id: 7}, function (err, user) {
|
||||||
|
assert(!err);
|
||||||
|
assert.equal(user.first, 'joe');
|
||||||
|
|
||||||
|
User.upsert({first: 'bob', id: 7}, function (err, updatedUser) {
|
||||||
|
assert(!err);
|
||||||
|
assert.equal(updatedUser.first, 'bob');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('model.destroy([callback])', function() {
|
||||||
|
it("Remove a model from the attached data source", function(done) {
|
||||||
|
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||||
|
User.findById(user.id, function (err, foundUser) {
|
||||||
|
assert.equal(user.id, foundUser.id);
|
||||||
|
foundUser.destroy(function () {
|
||||||
|
User.findById(user.id, function (err, notFound) {
|
||||||
|
assert.equal(notFound, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.deleteById(id, [callback])', function () {
|
||||||
|
it("Delete a model instance from the attached data source", function (done) {
|
||||||
|
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||||
|
User.deleteById(user.id, function (err) {
|
||||||
|
User.findById(user.id, function (err, notFound) {
|
||||||
|
assert.equal(notFound, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.findById(id, callback)', function() {
|
||||||
|
it("Find an instance by id", function(done) {
|
||||||
|
User.create({first: 'michael', last: 'jordan', id: 23}, function () {
|
||||||
|
User.findById(23, function (err, user) {
|
||||||
|
assert.equal(user.id, 23);
|
||||||
|
assert.equal(user.first, 'michael');
|
||||||
|
assert.equal(user.last, 'jordan');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model.count([query], callback)', function() {
|
||||||
|
it("Query count of Model instances in data source", function(done) {
|
||||||
|
(new TaskEmitter())
|
||||||
|
.task(User, 'create', {first: 'jill', age: 100})
|
||||||
|
.task(User, 'create', {first: 'bob', age: 200})
|
||||||
|
.task(User, 'create', {first: 'jan'})
|
||||||
|
.task(User, 'create', {first: 'sam'})
|
||||||
|
.task(User, 'create', {first: 'suzy'})
|
||||||
|
.on('done', function () {
|
||||||
|
User.count({age: {gt: 99}}, function (err, count) {
|
||||||
|
assert.equal(count, 2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue