Merge branch 'master' into feature/fix-issue-377
This commit is contained in:
commit
335bae4b46
|
@ -11,3 +11,5 @@
|
|||
*.swp
|
||||
*.swo
|
||||
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*/
|
||||
module.exports = function(grunt) {
|
||||
|
||||
grunt.loadNpmTasks('grunt-mocha-test');
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
// Metadata.
|
||||
|
@ -53,86 +55,39 @@ module.exports = function(grunt) {
|
|||
}
|
||||
}
|
||||
},
|
||||
karma: {
|
||||
unit: {
|
||||
mochaTest: {
|
||||
'unit': {
|
||||
src: 'test/*.js',
|
||||
options: {
|
||||
// base path, that will be used to resolve files and exclude
|
||||
basePath: '',
|
||||
|
||||
// frameworks to use
|
||||
frameworks: ['mocha', 'browserify'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'test/support.js',
|
||||
'test/model.test.js',
|
||||
'test/geo-point.test.js'
|
||||
],
|
||||
|
||||
// list of files to exclude
|
||||
exclude: [
|
||||
|
||||
],
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
|
||||
reporters: ['dots'],
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// cli runner port
|
||||
runnerPort: 9100,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: 'warn',
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
|
||||
// Start these browsers, currently available:
|
||||
// - Chrome
|
||||
// - ChromeCanary
|
||||
// - Firefox
|
||||
// - Opera
|
||||
// - Safari (only Mac)
|
||||
// - PhantomJS
|
||||
// - IE (only Windows)
|
||||
browsers: [
|
||||
'Chrome'
|
||||
],
|
||||
|
||||
// If browser does not capture in given timeout [ms], kill it
|
||||
captureTimeout: 60000,
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, it capture browsers, run tests and exit
|
||||
singleRun: false,
|
||||
|
||||
// Browserify config (all optional)
|
||||
browserify: {
|
||||
// extensions: ['.coffee'],
|
||||
ignore: [
|
||||
'nodemailer',
|
||||
'passport',
|
||||
'passport-local',
|
||||
'superagent',
|
||||
'supertest'
|
||||
],
|
||||
// transform: ['coffeeify'],
|
||||
// debug: true,
|
||||
// noParse: ['jquery'],
|
||||
watch: true,
|
||||
},
|
||||
|
||||
// Add browserify to preprocessors
|
||||
preprocessors: {'test/*': ['browserify']}
|
||||
reporter: 'dot',
|
||||
}
|
||||
},
|
||||
'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: {
|
||||
options: {
|
||||
// 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
|
||||
files: [
|
||||
'test/e2e/remote-connector.e2e.js'
|
||||
'test/e2e/remote-connector.e2e.js',
|
||||
'test/e2e/replication.e2e.js'
|
||||
],
|
||||
|
||||
// list of files to exclude
|
||||
|
@ -232,4 +188,10 @@ module.exports = function(grunt) {
|
|||
// Default task.
|
||||
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/runtime.js",
|
||||
"lib/registry.js",
|
||||
{ "title": "Base model", "depth": 2 },
|
||||
{ "title": "Base models", "depth": 2 },
|
||||
"lib/models/model.js",
|
||||
"lib/models/data-model.js",
|
||||
"lib/models/persisted-model.js",
|
||||
{ "title": "Middleware", "depth": 2 },
|
||||
"lib/middleware/rest.js",
|
||||
"lib/middleware/status.js",
|
||||
|
@ -19,7 +19,8 @@
|
|||
"lib/models/application.js",
|
||||
"lib/models/email.js",
|
||||
"lib/models/role.js",
|
||||
"lib/models/user.js"
|
||||
"lib/models/user.js",
|
||||
"lib/models/change.js"
|
||||
],
|
||||
"assets": "/docs/assets"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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},
|
||||
price: Number,
|
||||
item: String,
|
||||
|
@ -22,8 +22,7 @@ CartItem.sum = function(cartId, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
loopback.remoteMethod(
|
||||
CartItem.sum,
|
||||
CartItem.remoteMethod('sum',
|
||||
{
|
||||
accepts: {arg: 'cartId', 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
|
||||
, registry = require('./registry')
|
||||
, compat = require('./compat')
|
||||
, assert = require('assert')
|
||||
, fs = require('fs')
|
||||
, extend = require('util')._extend
|
||||
, _ = require('underscore')
|
||||
, RemoteObjects = require('strong-remoting')
|
||||
, swagger = require('strong-remoting/ext/swagger')
|
||||
, stringUtils = require('underscore.string')
|
||||
, path = require('path');
|
||||
|
||||
|
@ -94,7 +92,7 @@ app.disuse = function (route) {
|
|||
* var User = loopback.User;
|
||||
* 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', {
|
||||
* dataSource: 'db',
|
||||
* properties: {
|
||||
|
@ -152,13 +150,14 @@ app.model = function (Model, config) {
|
|||
|
||||
this.models().push(Model);
|
||||
|
||||
if (isPublic) {
|
||||
var remotingClassName = compat.getClassNameForRemoting(Model);
|
||||
this.remotes().exports[remotingClassName] = Model;
|
||||
if (isPublic && Model.sharedClass) {
|
||||
this.remotes().addClass(Model.sharedClass);
|
||||
if (Model.settings.trackChanges && Model.Change)
|
||||
this.remotes().addClass(Model.Change.sharedClass);
|
||||
clearHandlerCache(this);
|
||||
}
|
||||
|
||||
Model.shared = isPublic; // The base Model has shared = true
|
||||
Model.shared = isPublic;
|
||||
Model.app = this;
|
||||
Model.emit('attached', this);
|
||||
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()`
|
||||
*
|
||||
* **Deprecated. Use the package
|
||||
* [loopback-boot](https://github.com/strongloop/loopback-boot) instead.**
|
||||
|
||||
* There are two ways to access models:
|
||||
*
|
||||
* 1. Call `app.models()` to get a list of all models.
|
||||
|
@ -261,47 +257,14 @@ app.connector = function(name, connector) {
|
|||
|
||||
app.remoteObjects = function () {
|
||||
var result = {};
|
||||
var models = this.models();
|
||||
|
||||
// add in models
|
||||
models.forEach(function (ModelCtor) {
|
||||
// only add shared models
|
||||
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
|
||||
result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor;
|
||||
}
|
||||
|
||||
this.remotes().classes().forEach(function(sharedClass) {
|
||||
result[sharedClass.name] = sharedClass.ctor;
|
||||
});
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -381,207 +344,9 @@ app.enableAuth = function() {
|
|||
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) {
|
||||
options = options || {};
|
||||
|
||||
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]);
|
||||
});
|
||||
throw new Error(
|
||||
'`app.boot` was removed, use the new module loopback-boot instead');
|
||||
}
|
||||
|
||||
function classify(str) {
|
||||
|
@ -634,216 +399,10 @@ function configureModel(ModelCtor, config, app) {
|
|||
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) {
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,12 @@ MailConnector.prototype.setupTransport = function(setting) {
|
|||
var connector = this;
|
||||
connector.transports = connector.transports || [];
|
||||
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.transports.push(transport);
|
||||
}
|
||||
|
|
|
@ -36,4 +36,4 @@ inherits(Memory, Connector);
|
|||
* JugglingDB Compatibility
|
||||
*/
|
||||
|
||||
Memory.initialize = JdbMemory.initialize;
|
||||
Memory.initialize = JdbMemory.initialize;
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
* Dependencies.
|
||||
*/
|
||||
|
||||
var assert = require('assert')
|
||||
, compat = require('../compat')
|
||||
, _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var remoting = require('strong-remoting');
|
||||
var DataAccessObject = require('loopback-datasource-juggler/lib/dao');
|
||||
|
||||
/**
|
||||
* Export the RemoteConnector class.
|
||||
|
@ -24,6 +24,10 @@ function RemoteConnector(settings) {
|
|||
this.root = settings.root || '';
|
||||
this.host = settings.host || 'localhost';
|
||||
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) {
|
||||
this.url = settings.url;
|
||||
|
@ -31,14 +35,14 @@ function RemoteConnector(settings) {
|
|||
this.url = this.protocol + '://' + this.host + ':' + this.port + this.root;
|
||||
}
|
||||
|
||||
// handle mixins here
|
||||
this.DataAccessObject = function() {};
|
||||
// handle mixins in the define() method
|
||||
var DAO = this.DataAccessObject = function() {};
|
||||
}
|
||||
|
||||
RemoteConnector.prototype.connect = function() {
|
||||
this.remotes.connect(this.url, this.adapter);
|
||||
}
|
||||
|
||||
|
||||
RemoteConnector.initialize = function(dataSource, callback) {
|
||||
var connector = dataSource.connector = new RemoteConnector(dataSource.settings);
|
||||
connector.connect();
|
||||
|
@ -47,22 +51,40 @@ RemoteConnector.initialize = function(dataSource, callback) {
|
|||
|
||||
RemoteConnector.prototype.define = function(definition) {
|
||||
var Model = definition.model;
|
||||
var className = compat.getClassNameForRemoting(Model);
|
||||
var url = this.url;
|
||||
var adapter = this.adapter;
|
||||
var remotes = this.remotes;
|
||||
var SharedClass;
|
||||
|
||||
Model.remotes(function(err, remotes) {
|
||||
var sharedClass = getSharedClass(remotes, className);
|
||||
remotes.connect(url, adapter);
|
||||
sharedClass
|
||||
.methods()
|
||||
.forEach(Model.createProxyMethod.bind(Model));
|
||||
});
|
||||
assert(Model.sharedClass, 'cannot attach ' + Model.modelName
|
||||
+ ' to a remote connector without a Model.sharedClass');
|
||||
|
||||
remotes.addClass(Model.sharedClass);
|
||||
|
||||
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) {
|
||||
return _.find(remotes.classes(), function(sharedClass) {
|
||||
return sharedClass.name === className;
|
||||
});
|
||||
function createProxyMethod(Model, remotes, remoteMethod) {
|
||||
var scope = remoteMethod.isStatic ? Model : Model.prototype;
|
||||
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() {}
|
||||
|
|
|
@ -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')
|
||||
, fs = require('fs')
|
||||
, ejs = require('ejs')
|
||||
, EventEmitter = require('events').EventEmitter
|
||||
, path = require('path')
|
||||
, DataSource = require('loopback-datasource-juggler').DataSource
|
||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
||||
, i8n = require('inflection')
|
||||
, merge = require('util')._extend
|
||||
, assert = require('assert');
|
||||
|
||||
|
@ -42,11 +38,6 @@ loopback.version = require('../package.json').version;
|
|||
|
||||
loopback.mime = express.mime;
|
||||
|
||||
/*!
|
||||
* Compatibility layer, intentionally left undocumented.
|
||||
*/
|
||||
loopback.compat = require('./compat');
|
||||
|
||||
/*!
|
||||
* Create an loopback application.
|
||||
*
|
||||
|
@ -84,6 +75,10 @@ function createApplication() {
|
|||
function mixin(source) {
|
||||
for (var key in source) {
|
||||
var desc = Object.getOwnPropertyDescriptor(source, key);
|
||||
|
||||
// Fix for legacy (pre-ES5) browsers like PhantomJS
|
||||
if (!desc) continue;
|
||||
|
||||
Object.defineProperty(loopback, key, desc);
|
||||
}
|
||||
}
|
||||
|
@ -92,12 +87,22 @@ mixin(require('./runtime'));
|
|||
mixin(require('./registry'));
|
||||
|
||||
/*!
|
||||
* Expose express.middleware as loopback.*
|
||||
* for example `loopback.errorHandler` etc.
|
||||
* Expose static express methods like `express.errorHandler`.
|
||||
*/
|
||||
|
||||
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
|
||||
|
@ -168,6 +173,7 @@ loopback.Role = require('./models/role').Role;
|
|||
loopback.RoleMapping = require('./models/role').RoleMapping;
|
||||
loopback.ACL = require('./models/acl').ACL;
|
||||
loopback.Scope = require('./models/acl').Scope;
|
||||
loopback.Change = require('./models/change');
|
||||
|
||||
/*!
|
||||
* Automatically attach these models to dataSources
|
||||
|
@ -179,7 +185,7 @@ var dataSourceTypes = {
|
|||
};
|
||||
|
||||
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
||||
loopback.DataModel.autoAttach = dataSourceTypes.DB;
|
||||
loopback.PersistedModel.autoAttach = dataSourceTypes.DB;
|
||||
loopback.User.autoAttach = dataSourceTypes.DB;
|
||||
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Role.autoAttach = dataSourceTypes.DB;
|
||||
|
|
|
@ -92,7 +92,7 @@ var ACLSchema = {
|
|||
* @inherits Model
|
||||
*/
|
||||
|
||||
var ACL = loopback.createModel('ACL', ACLSchema);
|
||||
var ACL = loopback.PersistedModel.extend('ACL', ACLSchema);
|
||||
|
||||
ACL.ALL = AccessContext.ALL;
|
||||
|
||||
|
@ -261,7 +261,7 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
|
|||
* @return {Object[]} An array of ACLs
|
||||
*/
|
||||
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||
var modelClass = loopback.getModel(model);
|
||||
var modelClass = loopback.findModel(model);
|
||||
var staticACLs = [];
|
||||
if (modelClass && modelClass.settings.acls) {
|
||||
modelClass.settings.acls.forEach(function (acl) {
|
||||
|
@ -343,7 +343,7 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
|
|||
acls = acls.concat(dynACLs);
|
||||
resolved = self.resolvePermission(acls, req);
|
||||
if(resolved && resolved.permission === ACL.DEFAULT) {
|
||||
var modelClass = loopback.getModel(model);
|
||||
var modelClass = loopback.findModel(model);
|
||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
callback && callback(null, resolved);
|
||||
|
|
|
@ -113,7 +113,7 @@ function generateKey(hmacKey, algorithm, encoding) {
|
|||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
var Application = loopback.createModel('Application', ApplicationSchema);
|
||||
var Application = loopback.PersistedModel.extend('Application', ApplicationSchema);
|
||||
|
||||
/*!
|
||||
* 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.
|
||||
*/
|
||||
var registry = require('../registry');
|
||||
var compat = require('../compat');
|
||||
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
|
||||
* @param {Object} data
|
||||
* @property {String} modelName The name of the model
|
||||
* @property {DataSource} dataSource
|
||||
*/
|
||||
|
||||
var Model = module.exports = registry.modelBuilder.define('Model');
|
||||
|
||||
Model.shared = true;
|
||||
|
||||
/*!
|
||||
* Called when a model is extended.
|
||||
*/
|
||||
|
||||
Model.setup = function () {
|
||||
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) {
|
||||
var ModelCtor = this;
|
||||
|
||||
if(typeof data === 'function') {
|
||||
fn = data;
|
||||
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
|
||||
ModelCtor.beforeRemote = function (name, fn) {
|
||||
var self = this;
|
||||
if(this.app) {
|
||||
var remotes = this.app.remotes();
|
||||
var className = compat.getClassNameForRemoting(self);
|
||||
var className = self.modelName;
|
||||
remotes.before(className + '.' + name, function (ctx, next) {
|
||||
fn(ctx, ctx.result, next);
|
||||
});
|
||||
|
@ -85,7 +168,7 @@ Model.setup = function () {
|
|||
var self = this;
|
||||
if(this.app) {
|
||||
var remotes = this.app.remotes();
|
||||
var className = compat.getClassNameForRemoting(self);
|
||||
var className = self.modelName;
|
||||
remotes.after(className + '.' + name, function (ctx, next) {
|
||||
fn(ctx, ctx.result, next);
|
||||
});
|
||||
|
@ -97,19 +180,21 @@ Model.setup = function () {
|
|||
}
|
||||
};
|
||||
|
||||
// Map the prototype method to /:id with data in the body
|
||||
var idDesc = ModelCtor.modelName + ' id';
|
||||
ModelCtor.sharedCtor.accepts = [
|
||||
{arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc}
|
||||
// {arg: 'instance', type: 'object', http: {source: 'body'}}
|
||||
];
|
||||
// resolve relation functions
|
||||
sharedClass.resolve(function resolver(define) {
|
||||
var relations = ModelCtor.relations;
|
||||
if(!relations) return;
|
||||
// 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;
|
||||
};
|
||||
|
||||
|
@ -140,6 +225,7 @@ Model._ACL = function getACL(ACL) {
|
|||
* @param {String|Error} err The error object
|
||||
* @param {Boolean} allowed True if the request is allowed; false otherwise.
|
||||
*/
|
||||
|
||||
Model.checkAccess = function(token, modelId, sharedMethod, callback) {
|
||||
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||
token = token || ANONYMOUS;
|
||||
|
@ -200,10 +286,8 @@ Model._getAccessTypeForMethod = function(method) {
|
|||
return ACL.WRITE;
|
||||
case 'count':
|
||||
return ACL.READ;
|
||||
break;
|
||||
default:
|
||||
return ACL.EXECUTE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,48 +313,58 @@ Model.getApp = function(callback) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the Model's `RemoteObjects`.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {RemoteObjects} remoteObjects
|
||||
* @end
|
||||
* Enable remote invocation for the method with the given name.
|
||||
*
|
||||
* @param {String} name The name of the method.
|
||||
* ```js
|
||||
* // static method example (eg. Model.myMethod())
|
||||
* 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) {
|
||||
this.getApp(function(err, app) {
|
||||
callback(null, app.remotes());
|
||||
});
|
||||
Model.remoteMethod = function(name, options) {
|
||||
if(options.isStatic === undefined) {
|
||||
options.isStatic = true;
|
||||
}
|
||||
this.sharedClass.defineMethod(name, options);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Create a proxy function for invoking remote methods.
|
||||
*
|
||||
* @param {SharedMethod} sharedMethod
|
||||
*/
|
||||
Model.belongsToRemoting = function(relationName, relation, define) {
|
||||
var fn = this.prototype[relationName];
|
||||
define('__get__' + relationName, {
|
||||
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) {
|
||||
var Model = this;
|
||||
var scope = remoteMethod.isStatic ? Model : Model.prototype;
|
||||
var original = scope[remoteMethod.name];
|
||||
|
||||
var fn = scope[remoteMethod.name] = function proxy() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
|
||||
var callback;
|
||||
if(lastArgIsFunc) {
|
||||
callback = args.pop();
|
||||
}
|
||||
|
||||
Model.remotes(function(err, remotes) {
|
||||
remotes.invoke(remoteMethod.stringName, args, callback);
|
||||
});
|
||||
}
|
||||
|
||||
for(var key in original) {
|
||||
fn[key] = original[key];
|
||||
}
|
||||
fn._delegate = true;
|
||||
Model.scopeRemoting = function(relationName, relation, define) {
|
||||
var toModelName = relation.modelTo.modelName;
|
||||
|
||||
define('__get__' + relationName, {
|
||||
isStatic: false,
|
||||
http: {verb: 'get', path: '/' + relationName},
|
||||
accepts: {arg: 'filter', type: 'object'},
|
||||
description: 'Queries ' + relationName + ' of ' + this.modelName + '.',
|
||||
returns: {arg: relationName, type: [toModelName], root: true}
|
||||
});
|
||||
|
||||
define('__create__' + relationName, {
|
||||
isStatic: false,
|
||||
http: {verb: 'post', path: '/' + relationName},
|
||||
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}
|
||||
});
|
||||
|
||||
define('__delete__' + relationName, {
|
||||
isStatic: false,
|
||||
http: {verb: 'delete', path: '/' + relationName},
|
||||
description: 'Deletes all ' + relationName + ' of this 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.
|
||||
*/
|
||||
|
||||
var loopback = require('../loopback')
|
||||
, Model = loopback.Model
|
||||
var PersistedModel = require('../loopback').PersistedModel
|
||||
, loopback = require('../loopback')
|
||||
, path = require('path')
|
||||
, SALT_WORK_FACTOR = 10
|
||||
, crypto = require('crypto')
|
||||
|
@ -54,7 +54,7 @@ var options = {
|
|||
principalType: ACL.ROLE,
|
||||
principalId: Role.OWNER,
|
||||
permission: ACL.ALLOW,
|
||||
property: 'removeById'
|
||||
property: 'deleteById'
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
|
@ -103,7 +103,7 @@ var options = {
|
|||
*
|
||||
* - DENY EVERYONE `*`
|
||||
* - ALLOW EVERYONE `create`
|
||||
* - ALLOW OWNER `removeById`
|
||||
* - ALLOW OWNER `deleteById`
|
||||
* - ALLOW EVERYONE `login`
|
||||
* - ALLOW EVERYONE `logout`
|
||||
* - ALLOW EVERYONE `findById`
|
||||
|
@ -113,7 +113,7 @@ var options = {
|
|||
* @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
|
||||
|
@ -434,7 +434,7 @@ User.resetPassword = function(options, cb) {
|
|||
|
||||
User.setup = function () {
|
||||
// We need to call the base class's setup method
|
||||
Model.setup.call(this);
|
||||
PersistedModel.setup.call(this);
|
||||
var UserModel = this;
|
||||
|
||||
// max ttl
|
||||
|
@ -458,6 +458,7 @@ User.setup = function () {
|
|||
loopback.remoteMethod(
|
||||
UserModel.login,
|
||||
{
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
|
||||
{arg: 'include', type: 'string', http: {source: 'query' }, description:
|
||||
|
@ -478,6 +479,7 @@ User.setup = function () {
|
|||
loopback.remoteMethod(
|
||||
UserModel.logout,
|
||||
{
|
||||
description: 'Logout a user with access token',
|
||||
accepts: [
|
||||
{arg: 'access_token', type: 'string', required: true, http: function(ctx) {
|
||||
var req = ctx && ctx.req;
|
||||
|
@ -497,6 +499,7 @@ User.setup = function () {
|
|||
loopback.remoteMethod(
|
||||
UserModel.confirm,
|
||||
{
|
||||
description: 'Confirm a user registration with email verification token',
|
||||
accepts: [
|
||||
{arg: 'uid', type: 'string', required: true},
|
||||
{arg: 'token', type: 'string', required: true},
|
||||
|
@ -509,6 +512,7 @@ User.setup = function () {
|
|||
loopback.remoteMethod(
|
||||
UserModel.resetPassword,
|
||||
{
|
||||
description: 'Reset password for a user with email',
|
||||
accepts: [
|
||||
{arg: 'options', type: 'object', required: true, http: {source: 'body'}}
|
||||
],
|
||||
|
@ -530,10 +534,12 @@ User.setup = function () {
|
|||
UserModel.email = require('./email');
|
||||
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,}))$/;
|
||||
|
||||
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
||||
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
||||
|
||||
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);
|
||||
|
||||
|
@ -180,10 +180,26 @@ registry.configureModel = function(ModelCtor, config) {
|
|||
* @param {String} modelName The model name
|
||||
* @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)
|
||||
*/
|
||||
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.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.
|
||||
registry.modelBuilder.defaultModelBaseClass = registry.Model;
|
||||
|
|
51
package.json
51
package.json
|
@ -26,48 +26,59 @@
|
|||
"mobile",
|
||||
"mBaaS"
|
||||
],
|
||||
"version": "1.10.0",
|
||||
"version": "2.0.0-beta7",
|
||||
"scripts": {
|
||||
"test": "mocha -R spec"
|
||||
"test": "grunt mocha-and-karma"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "~0.9.0",
|
||||
"bcryptjs": "~2.0.1",
|
||||
"debug": "~1.0.3",
|
||||
"body-parser": "~1.4.3",
|
||||
"canonical-json": "0.0.4",
|
||||
"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",
|
||||
"nodemailer": "~0.7.1",
|
||||
"strong-remoting": "~1.5.1",
|
||||
"nodemailer": "~1.0.1",
|
||||
"nodemailer-stub-transport": "~0.1.4",
|
||||
"uid2": "0.0.3",
|
||||
"underscore": "~1.6.0",
|
||||
"underscore.string": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"loopback-datasource-juggler": "^1.7.0"
|
||||
"loopback-datasource-juggler": "~2.0.0-beta3"
|
||||
},
|
||||
"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",
|
||||
"chai": "~1.9.1",
|
||||
"cookie-parser": "~1.3.2",
|
||||
"errorhandler": "~1.1.1",
|
||||
"es5-shim": "^4.0.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-browserify": "~2.1.3",
|
||||
"grunt-contrib-uglify": "~0.5.0",
|
||||
"grunt-cli": "^0.1.13",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-contrib-uglify": "~0.5.0",
|
||||
"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-firefox-launcher": "~0.1.3",
|
||||
"karma-html2js-preprocessor": "~0.1.0",
|
||||
"karma-junit-reporter": "^0.2.2",
|
||||
"karma-mocha": "^0.1.4",
|
||||
"karma-phantomjs-launcher": "~0.1.4",
|
||||
"karma": "~0.12.17",
|
||||
"karma-browserify": "~0.2.1",
|
||||
"karma-mocha": "~0.1.4",
|
||||
"grunt-karma": "~0.8.3"
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
"loopback-boot": "^1.1.0",
|
||||
"loopback-datasource-juggler": "~2.0.0-beta3",
|
||||
"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": {
|
||||
"type": "git",
|
||||
|
|
|
@ -200,7 +200,7 @@ function createTestApp(testToken, settings, done) {
|
|||
principalId: "$everyone",
|
||||
accessType: ACL.ALL,
|
||||
permission: ACL.DENY,
|
||||
property: 'removeById'
|
||||
property: 'deleteById'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -209,7 +209,7 @@ function createTestApp(testToken, settings, done) {
|
|||
modelOptions[key] = modelSettings[key];
|
||||
});
|
||||
|
||||
var TestModel = loopback.Model.extend('test', {}, modelOptions);
|
||||
var TestModel = loopback.PersistedModel.extend('test', {}, modelOptions);
|
||||
|
||||
TestModel.attachTo(loopback.memory());
|
||||
app.model(TestModel);
|
||||
|
|
|
@ -21,7 +21,8 @@ before(function() {
|
|||
|
||||
describe('security scopes', 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);
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
|
|
359
test/app.test.js
359
test/app.test.js
|
@ -1,5 +1,10 @@
|
|||
var path = require('path');
|
||||
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() {
|
||||
|
||||
|
@ -11,22 +16,26 @@ describe('app', 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);
|
||||
Color.attachTo(db);
|
||||
|
||||
expect(app.models()).to.eql([Color]);
|
||||
});
|
||||
|
||||
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);
|
||||
Color.attachTo(db);
|
||||
expect(app.remoteObjects()).to.eql({ color: Color });
|
||||
});
|
||||
|
||||
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);
|
||||
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() {
|
||||
|
@ -38,36 +47,16 @@ describe('app', function() {
|
|||
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());
|
||||
request(app).get('/colors').expect(404, function(err, res) {
|
||||
if (err) return done(err);
|
||||
var Color = db.createModel('color', {name: String});
|
||||
var Color = PersistedModel.extend('color', {name: String});
|
||||
app.model(Color);
|
||||
Color.attachTo(db);
|
||||
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 () {
|
||||
|
@ -75,13 +64,8 @@ describe('app', function() {
|
|||
|
||||
beforeEach(function() {
|
||||
app = loopback();
|
||||
app.boot({
|
||||
app: {port: 3000, host: '127.0.0.1'},
|
||||
dataSources: {
|
||||
db: {
|
||||
connector: 'memory'
|
||||
}
|
||||
}
|
||||
app.dataSource('db', {
|
||||
connector: 'memory'
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -177,286 +161,7 @@ describe('app', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('app.boot([options])', 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() {
|
||||
describe.onServer('listen()', function() {
|
||||
it('starts http server', function(done) {
|
||||
var app = loopback();
|
||||
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() {
|
||||
expect(app.isAuthEnabled).to.not.equal(true);
|
||||
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) {
|
||||
var app = loopback();
|
||||
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() {
|
||||
var app = 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() {
|
||||
it("List the enabled and disabled operations", function() {
|
||||
describe.skip('PersistedModel Methods', function() {
|
||||
it("List the enabled and disabled methods", function() {
|
||||
var TestModel = loopback.PersistedModel.extend('TestPersistedModel');
|
||||
TestModel.attachTo(loopback.memory());
|
||||
|
||||
// assert the defaults
|
||||
// - true: the method should be remote enabled
|
||||
// - false: the method should not be remote enabled
|
||||
// -
|
||||
// -
|
||||
existsAndShared('_forDB', false);
|
||||
existsAndShared('create', true);
|
||||
existsAndShared('updateOrCreate', true);
|
||||
|
@ -61,11 +64,15 @@ describe('DataSource', function() {
|
|||
existsAndShared('destroyById', true);
|
||||
existsAndShared('destroy', false);
|
||||
existsAndShared('updateAttributes', true);
|
||||
existsAndShared('updateAll', true);
|
||||
existsAndShared('reload', false);
|
||||
|
||||
function existsAndShared(name, isRemoteEnabled) {
|
||||
var op = memory.getOperation(name);
|
||||
assert(op.remoteEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled');
|
||||
function existsAndShared(Model, name, isRemoteEnabled, isProto) {
|
||||
var scope = isProto ? Model.prototype : Model;
|
||||
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() {
|
||||
before(function() {
|
||||
// setup the remote connector
|
||||
var localApp = loopback();
|
||||
var ds = loopback.createDataSource({
|
||||
url: 'http://localhost:3000/api',
|
||||
connector: loopback.Remote
|
||||
});
|
||||
localApp.model(TestModel);
|
||||
TestModel.attachTo(ds);
|
||||
});
|
||||
|
||||
|
@ -32,7 +30,7 @@ describe('RemoteConnector', function() {
|
|||
});
|
||||
m.save(function(err, data) {
|
||||
if(err) return done(err);
|
||||
assert(m.id);
|
||||
assert(data.foo === 'bar');
|
||||
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) {
|
||||
assert(mail.message);
|
||||
assert(!err);
|
||||
assert(mail.response);
|
||||
assert(mail.envelope);
|
||||
assert(mail.messageId);
|
||||
done(err);
|
||||
|
@ -41,7 +42,7 @@ describe('Email and SMTP', function () {
|
|||
});
|
||||
|
||||
message.send(function (err, mail) {
|
||||
assert(mail.message);
|
||||
assert(mail.response);
|
||||
assert(mail.envelope);
|
||||
assert(mail.messageId);
|
||||
done(err);
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
var loopback = require('../../../');
|
||||
var boot = require('loopback-boot');
|
||||
var path = require('path');
|
||||
var app = module.exports = loopback();
|
||||
|
||||
app.boot(__dirname);
|
||||
boot(app, __dirname);
|
||||
|
||||
var apiPath = '/api';
|
||||
app.use(loopback.cookieParser('secret'));
|
||||
app.use(loopback.token({model: app.models.accessToken}));
|
||||
app.use(apiPath, loopback.rest());
|
||||
app.use(app.router);
|
||||
|
||||
app.use(loopback.urlNotFound());
|
||||
app.use(loopback.errorHandler());
|
||||
app.enableAuth();
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
"permission": "DENY",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"property": "removeById"
|
||||
"property": "deleteById"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -3,12 +3,18 @@ var path = require('path');
|
|||
var app = module.exports = loopback();
|
||||
var models = require('./models');
|
||||
var TestModel = models.TestModel;
|
||||
// var explorer = require('loopback-explorer');
|
||||
|
||||
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
||||
var apiPath = '/api';
|
||||
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.urlNotFound());
|
||||
app.use(loopback.errorHandler());
|
||||
app.model(TestModel);
|
||||
TestModel.attachTo(loopback.memory());
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
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 boot = require('loopback-boot');
|
||||
var path = require('path');
|
||||
var app = module.exports = loopback();
|
||||
|
||||
app.boot(__dirname);
|
||||
boot(app, __dirname);
|
||||
app.use(loopback.favicon());
|
||||
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
|
||||
var apiPath = '/api';
|
||||
|
|
|
@ -3,15 +3,20 @@ var loopback = require('../');
|
|||
describe('hidden properties', function () {
|
||||
beforeEach(function (done) {
|
||||
var app = this.app = loopback();
|
||||
var Product = this.Product = app.model('product', {
|
||||
options: {hidden: ['secret']},
|
||||
dataSource: loopback.memory()
|
||||
});
|
||||
var Category = this.Category = this.app.model('category', {
|
||||
dataSource: loopback.memory()
|
||||
});
|
||||
var Product = this.Product = loopback.PersistedModel.extend('product',
|
||||
{},
|
||||
{hidden: ['secret']}
|
||||
);
|
||||
Product.attachTo(loopback.memory());
|
||||
|
||||
var Category = this.Category = loopback.PersistedModel.extend('category');
|
||||
Category.attachTo(loopback.memory());
|
||||
Category.hasMany(Product);
|
||||
|
||||
app.model(Product);
|
||||
app.model(Category);
|
||||
app.use(loopback.rest());
|
||||
|
||||
Category.create({
|
||||
name: 'my 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('MyCustomModel') === MyCustomModel);
|
||||
assert(loopback.getModel('Invalid') === undefined);
|
||||
assert(loopback.findModel('Invalid') === undefined);
|
||||
});
|
||||
it('should be able to get model by type', function () {
|
||||
var MyModel = loopback.createModel('MyModel', {}, {
|
||||
|
@ -141,6 +141,11 @@ describe('loopback', function() {
|
|||
assert(loopback.getModelByType(MyModel) === 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 ACL = require('../').ACL;
|
||||
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('Model', function() {
|
||||
var describe = require('./util/describe');
|
||||
|
||||
var User, memory;
|
||||
|
||||
beforeEach(function () {
|
||||
memory = loopback.createDataSource({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 / PersistedModel', function() {
|
||||
defineModelTestsWithDataSource({
|
||||
dataSource: {
|
||||
connector: loopback.Memory
|
||||
}
|
||||
});
|
||||
|
||||
describe('Model.validatesUniquenessOf(property, options)', function() {
|
||||
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'});
|
||||
|
||||
var joe = new User({email: 'joe@joe.com'});
|
||||
var joe2 = new User({email: 'joe@joe.com'});
|
||||
|
||||
|
||||
joe.save(function () {
|
||||
joe2.save(function (err) {
|
||||
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() {
|
||||
it("Attach a model to a [DataSource](#data-source)", function() {
|
||||
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);
|
||||
|
||||
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();
|
||||
MyModel.find(function(err, results) {
|
||||
assert(results.length === 0, 'should have data access methods after attaching to a data source');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.onServer('Remote Methods', function(){
|
||||
|
||||
var User;
|
||||
var dataSource;
|
||||
var app;
|
||||
|
||||
beforeEach(function () {
|
||||
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() {
|
||||
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();
|
||||
});
|
||||
});
|
||||
dataSource = loopback.createDataSource({
|
||||
connector: loopback.Memory
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
User.attachTo(dataSource);
|
||||
|
||||
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(!err);
|
||||
assert.equal(notFound, null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
User.login = function (username, password, fn) {
|
||||
if(username === 'foo' && password === 'bar') {
|
||||
fn(null, 123);
|
||||
} else {
|
||||
throw new Error('bad username and password!');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Model.deleteById([callback])', function () {
|
||||
it("Delete a model instance from the attached data source", function (done) {
|
||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||
User.deleteById(user.id, function (err) {
|
||||
User.findById(user.id, function (err, notFound) {
|
||||
assert(!err);
|
||||
assert.equal(notFound, null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
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 = loopback();
|
||||
app.use(loopback.rest());
|
||||
app.model(User);
|
||||
});
|
||||
|
||||
describe('Model.destroyAll(callback)', function() {
|
||||
it("Delete all Model instances from data source", function(done) {
|
||||
(new TaskEmitter())
|
||||
|
@ -220,7 +124,6 @@ describe('Model', function() {
|
|||
.task(User, 'create', {first: 'suzy'})
|
||||
.on('done', function () {
|
||||
User.count(function (err, count) {
|
||||
assert.equal(count, 5);
|
||||
User.destroyAll(function () {
|
||||
User.count(function (err, count) {
|
||||
assert.equal(count, 0);
|
||||
|
@ -232,93 +135,97 @@ describe('Model', function() {
|
|||
});
|
||||
});
|
||||
|
||||
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');
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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() {
|
||||
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();
|
||||
});
|
||||
describe('Model.beforeRemote(name, fn)', function(){
|
||||
it('Run a function before a remote method is called by a client', function(done) {
|
||||
var hookCalled = false;
|
||||
|
||||
User.beforeRemote('create', function(ctx, user, next) {
|
||||
hookCalled = 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(hookCalled, 'hook wasnt called');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.onServer('Remote Methods', function(){
|
||||
|
||||
beforeEach(function () {
|
||||
User.login = function (username, password, fn) {
|
||||
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'}
|
||||
}
|
||||
);
|
||||
describe('Model.afterRemote(name, fn)', function(){
|
||||
it('Run a function after a remote method is called by a client', function(done) {
|
||||
var beforeCalled = false;
|
||||
var afterCalled = false;
|
||||
|
||||
app.use(loopback.rest());
|
||||
app.model(User);
|
||||
});
|
||||
|
||||
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.beforeRemote('create', function(ctx, user, next) {
|
||||
assert(!afterCalled);
|
||||
beforeCalled = true;
|
||||
next();
|
||||
});
|
||||
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) {
|
||||
request(app)
|
||||
.get('/users/not-found')
|
||||
.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) {
|
||||
describe('Remote Method invoking context', function () {
|
||||
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')
|
||||
|
@ -327,28 +234,27 @@ describe('Model', function() {
|
|||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
if(err) return done(err);
|
||||
assert(hookCalled, 'hook wasnt called');
|
||||
assert(hookCalled);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model.afterRemote(name, fn)', function(){
|
||||
it('Run a function after a remote method is called by a client', function(done) {
|
||||
var beforeCalled = false;
|
||||
var afterCalled = false;
|
||||
|
||||
describe('ctx.res', function() {
|
||||
it("The express ServerResponse object", function(done) {
|
||||
var hookCalled = false;
|
||||
|
||||
User.beforeRemote('create', function(ctx, user, next) {
|
||||
assert(!afterCalled);
|
||||
beforeCalled = 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();
|
||||
});
|
||||
User.afterRemote('create', function(ctx, user, next) {
|
||||
assert(beforeCalled);
|
||||
afterCalled = true;
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
// invoke save
|
||||
request(app)
|
||||
.post('/users')
|
||||
|
@ -357,115 +263,17 @@ describe('Model', function() {
|
|||
.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');
|
||||
assert(hookCalled);
|
||||
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() {
|
||||
it("Define a one to many relationship", function(done) {
|
||||
var Book = memory.createModel('book', {title: String, author: String});
|
||||
var Chapter = memory.createModel('chapter', {title: String});
|
||||
var Book = dataSource.createModel('book', {title: String, author: String});
|
||||
var Chapter = dataSource.createModel('chapter', {title: String});
|
||||
|
||||
// by referencing model
|
||||
Book.hasMany(Chapter);
|
||||
|
@ -523,7 +331,7 @@ describe('Model', function() {
|
|||
|
||||
describe('Model.extend()', 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
|
||||
});
|
||||
|
||||
|
@ -557,33 +365,33 @@ describe('Model', function() {
|
|||
|
||||
describe('Model.extend() events', function() {
|
||||
it('create isolated emitters for subclasses', function() {
|
||||
var User1 = loopback.createModel('User1', {
|
||||
'first': String,
|
||||
'last': String
|
||||
});
|
||||
var User1 = loopback.createModel('User1', {
|
||||
'first': String,
|
||||
'last': String
|
||||
});
|
||||
|
||||
var User2 = loopback.createModel('User2', {
|
||||
'name': String
|
||||
});
|
||||
var User2 = loopback.createModel('User2', {
|
||||
'name': String
|
||||
});
|
||||
|
||||
var user1Triggered = false;
|
||||
User1.once('x', function(event) {
|
||||
user1Triggered = true;
|
||||
});
|
||||
var user1Triggered = false;
|
||||
User1.once('x', function(event) {
|
||||
user1Triggered = true;
|
||||
});
|
||||
|
||||
|
||||
var user2Triggered = false;
|
||||
User2.once('x', function(event) {
|
||||
user2Triggered = true;
|
||||
});
|
||||
var user2Triggered = false;
|
||||
User2.once('x', function(event) {
|
||||
user2Triggered = true;
|
||||
});
|
||||
|
||||
assert(User1.once !== User2.once);
|
||||
assert(User1.once !== loopback.Model.once);
|
||||
assert(User1.once !== User2.once);
|
||||
assert(User1.once !== loopback.Model.once);
|
||||
|
||||
User1.emit('x', User1);
|
||||
User1.emit('x', User1);
|
||||
|
||||
assert(user1Triggered);
|
||||
assert(!user2Triggered);
|
||||
assert(user1Triggered);
|
||||
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() {
|
||||
it('should return the subclass of ACL', function() {
|
||||
var Model = require('../').Model;
|
||||
|
|
|
@ -378,7 +378,6 @@ describe('relations - integration', function () {
|
|||
it.skip('allows to find related objects via where filter', function(done) {
|
||||
//TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94
|
||||
var expectedProduct = this.product;
|
||||
// Note: the URL format is not final
|
||||
this.get('/api/products?filter[where][categoryId]=' + this.category.id)
|
||||
.expect(200, function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
|
|
@ -1,42 +1,70 @@
|
|||
var loopback = require('../');
|
||||
var defineModelTestsWithDataSource = require('./util/model-tests');
|
||||
|
||||
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) {
|
||||
var LocalModel = this.LocalModel = loopback.DataModel.extend('LocalModel');
|
||||
var RemoteModel = loopback.DataModel.extend('LocalModel');
|
||||
var localApp = loopback();
|
||||
var remoteApp = loopback();
|
||||
localApp.model(LocalModel);
|
||||
remoteApp.model(RemoteModel);
|
||||
var test = this;
|
||||
remoteApp = this.remoteApp = loopback();
|
||||
remoteApp.use(loopback.rest());
|
||||
RemoteModel.attachTo(loopback.memory());
|
||||
var ServerModel = this.ServerModel = loopback.PersistedModel.extend('TestModel');
|
||||
|
||||
remoteApp.model(ServerModel);
|
||||
|
||||
remoteApp.listen(0, function() {
|
||||
var ds = loopback.createDataSource({
|
||||
test.remote = loopback.createDataSource({
|
||||
host: remoteApp.get('host'),
|
||||
port: remoteApp.get('port'),
|
||||
connector: loopback.Remote
|
||||
});
|
||||
|
||||
LocalModel.attachTo(ds);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should alow methods to be called remotely', function (done) {
|
||||
var data = {foo: 'bar'};
|
||||
this.LocalModel.create(data, function(err, result) {
|
||||
if(err) return done(err);
|
||||
expect(result).to.deep.equal({id: 1, foo: 'bar'});
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should support the save method', function (done) {
|
||||
var calledServerCreate = false;
|
||||
var RemoteModel = loopback.PersistedModel.extend('TestModel');
|
||||
RemoteModel.attachTo(this.remote);
|
||||
|
||||
it('should alow instance methods to be called remotely', function (done) {
|
||||
var data = {foo: 'bar'};
|
||||
var m = new this.LocalModel(data);
|
||||
m.save(function(err, result) {
|
||||
if(err) return done(err);
|
||||
expect(result).to.deep.equal({id: 2, foo: 'bar'});
|
||||
var ServerModel = this.ServerModel;
|
||||
|
||||
ServerModel.create = function(data, cb) {
|
||||
calledServerCreate = true;
|
||||
data.id = 1;
|
||||
cb(null, data);
|
||||
}
|
||||
|
||||
ServerModel.setupRemoting();
|
||||
|
||||
var m = new RemoteModel({foo: 'bar'});
|
||||
m.save(function(err, inst) {
|
||||
assert(inst instanceof RemoteModel);
|
||||
assert(calledServerCreate);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('remoting - integration', function () {
|
|||
it("should load remoting options", function () {
|
||||
var remotes = app.remotes();
|
||||
assert.deepEqual(remotes.options, {"json": {"limit": "1kb", "strict": false},
|
||||
"urlencoded": {"limit": "8kb"}});
|
||||
"urlencoded": {"limit": "8kb", "extended": true}});
|
||||
});
|
||||
|
||||
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;
|
||||
TaskEmitter = require('strong-task-emitter');
|
||||
request = require('supertest');
|
||||
var RemoteObjects = require('strong-remoting');
|
||||
|
||||
// Speed up the password hashing algorithm
|
||||
// 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(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) {
|
||||
User.create({email: 'b@c.com'}, function (err) {
|
||||
|
@ -443,9 +452,9 @@ describe('User', function(){
|
|||
|
||||
user.verify(options, function (err, result) {
|
||||
assert(result.email);
|
||||
assert(result.email.message);
|
||||
assert(result.email.response);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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