Improve browser example

This commit is contained in:
Ritchie Martori 2014-02-04 16:52:34 -08:00
parent 60d155353a
commit 7712ebc898
11 changed files with 1128 additions and 225 deletions

917
dist/loopback.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -14,29 +14,48 @@ app.dataSource('local', {
});
var Color = loopback.getModel('Color');
var LocalColor = app.model('LocalColor', {dataSource: 'local'});
var LocalColor = app.model('LocalColor', {
dataSource: 'local',
options: {trackChanges: true}
});
LocalColor.create([
{name: 'red'},
{name: 'green'},
{name: 'blue'}
], function() {
LocalColor.replicate(0, Color, {}, function() {
console.log(arguments);
function replicate() {
LocalColor.currentCheckpoint(function(err, cp) {
LocalColor.replicate(cp, Color, {}, function() {
console.log('replicated local to remote');
});
});
});
}
LocalColor.on('deleted', replicate);
LocalColor.on('changed', replicate);
LocalColor.on('deletedAll', replicate);
// Color.create([
// {name: 'red'},
// {name: 'green'},
// {name: 'blue'}
// ], function() {
// Color.find(function(err, colors) {
// console.log(colors);
// });
// });
setInterval(function() {
Color.currentCheckpoint(function(err, cp) {
Color.replicate(cp, LocalColor, {}, function() {
console.log('replicated remote to local');
});
});
}, 100);
function ListCtrl($scope) {
LocalColor.on('changed', update);
LocalColor.on('deleted', update);
function update() {
LocalColor.find({sort: 'name'}, function(err, colors) {
$scope.colors = colors;
$scope.$apply();
});
}
$scope.add = function() {
LocalColor.create({name: $scope.newColor});
$scope.newColor = null;
}
update();
}
// Color.find({where: {name: 'green'}}, function(err, colors) {
// console.log(colors);
// });

View File

@ -1,13 +1,28 @@
<!doctype html>
<html lang="en" ng-app="myApp">
<html lang="en" ng-app>
<head>
<meta charset="utf-8">
<title>My AngularJS App</title>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<style>
body {font-size: 24px;}
</style>
<ul ng-controller="ListCtrl">
<li ng-repeat="color in colors" style="color: {{color.name}}">
{{color.name}}
</li>
<li>
<form ng-submit="add()">
<input placeholder="enter a color" ng-model="newColor" />
</form>
</li>
</ul>
<script src="loopback.js"></script>
<script src="loopback-remote-models.js"></script>
<script src="client.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
</body>
</html>

View File

@ -2,10 +2,11 @@ var loopback = require('../../');
var app = loopback();
var path = require('path');
app.use(loopback.logger(app.get('env') === 'development' ? 'dev' : 'default'));
app.use(loopback.static(path.join(__dirname, '..', '..', 'dist')));
app.use(loopback.static(path.join(__dirname)));
app.get('/loopback-remote-models.js', loopback.routes(app));
app.use(loopback.rest());
app.get('/loopback-remote-models.js', loopback.models(app));
app.use('/api', loopback.rest());
app.dataSource('db', {
connector: loopback.Memory
@ -16,5 +17,6 @@ var Color = app.model('Color', {dataSource: 'db', options: {
}});
app.model(Color.getChangeModel());
app.model(Color.getChangeModel().getCheckpointModel());
app.listen(3000);

View File

@ -12,7 +12,9 @@ var datasourceJuggler = require('loopback-datasource-juggler');
loopback.Connector = require('./lib/connectors/base-connector');
loopback.Memory = require('./lib/connectors/memory');
loopback.Mail = require('./lib/connectors/mail');
loopback.Server = require('./lib/connectors/server');
if(loopback.isBrowser) {
loopback.Server = require('./lib/connectors/server');
}
/**
* Types

View File

@ -5,7 +5,10 @@
var assert = require('assert')
, loopback = require('../loopback')
, debug = require('debug')
, path = require('path');
, path = require('path')
, request = require('browser-request')
, Connector = require('loopback-datasource-juggler').Connector
, util = require('util');
/*!
* Export the ServerConnector class.
@ -17,30 +20,67 @@ module.exports = ServerConnector;
* Create an instance of the connector with the given `settings`.
*/
function ServerConnector(settings) {
function ServerConnector(settings, dataSource) {
Connector.call(this, 'server', settings);
this.settings = settings;
this.dataSource = dataSource;
dataSource.DataAccessObject = dataSource.constructor.DataAccessObject;
settings.base = settings.base || '/';
dataSource.connect = this.connect;
}
util.inherits(ServerConnector, Connector);
ServerConnector.initialize = function(dataSource, callback) {
var connector = dataSource.connector = new ServerConnector(dataSource.settings);
connector.dataSource = dataSource;
dataSource.DataAccessObject = function() {}; // unused for this connector
var connector = dataSource.connector = new ServerConnector(dataSource.settings, dataSource);
var remoteModels = connector.settings.discover;
if(remoteModels) {
remoteModels = remoteModels.sort(function(remoteModel) {
var settings = remoteModel.settings;
var trackChanges = settings && settings.trackChanges;
return trackChanges ? 1 : 0;
});
remoteModels.forEach(connector.buildModel.bind(connector));
}
callback();
}
ServerConnector.prototype.invoke = function(ctx, callback) {
var req = ctx.toRequest();
console.log(req);
ServerConnector.prototype.connect = function(callback) {
process.nextTick(function () {
callback && callback(null, self.db);
});
}
ServerConnector.prototype.createRequest = function(method, args) {
var baseUrl = path.join(this.settings.base || '/');
var route = (method.routes && method.routes[0]) || {path: '/'};
var url = path.join(baseUrl, route.path);
ServerConnector.prototype.requestModel = function(model, req, callback) {
var Model = loopback.getModel(model);
var modelPath = '/' + Model.pluralModelName;
var url = path.join(this.settings.base, modelPath, req.url || '');
this.request(url, req, callback);
}
ServerConnector.prototype.requestModelById = function(model, id, req, callback) {
var Model = loopback.getModel(model);
var modelPath = '/' + Model.pluralModelName;
var url = path.join(this.settings.base, modelPath, id.toString(), req.url || '');
this.request(url, req, callback);
}
ServerConnector.prototype.request = function(url, req, callback) {
request({
url: url,
method: req.method || 'GET',
body: req.body,
json: req.json || true
}, function(err, res, body) {
if(res.statusCode >= 400) {
if(res.statusCode === 404 && req.ignoreNotFound) {
return callback && callback(null, null);
}
err = body.error || body;
body = undefined;
}
callback && callback(err, body);
});
}
ServerConnector.prototype.buildModel = function(remoteModel) {
@ -56,12 +96,19 @@ ServerConnector.prototype.buildModel = function(remoteModel) {
Model.attachTo(dataSource);
return;
if(!Model.defineMethod) {
Model.defineMethod = function defineMethod(method) {
var scope = method.fullName.indexOf('.prototype.') > -1
? Model.prototype : Model;
var isStatic = method.fullName.indexOf('.prototype.') === -1;
var scope = isStatic ? Model : Model.prototype;
var methodName = isStatic ? method.name : method.name.replace('prototype.', '');
scope[method.name] = function() {
if(methodName === 'Change') {
return; // skip
}
scope[methodName] = function() {
console.log(method.name);
var callback = arguments[arguments.length - 1];
var ctx = new Context(
@ -69,10 +116,10 @@ ServerConnector.prototype.buildModel = function(remoteModel) {
remoteModel,
Model,
method,
arguments
arguments,
callback
);
if(typeof callback !== 'function') callback = undefined;
connector.invoke(ctx, callback);
ctx.invoke();
};
}
}
@ -80,12 +127,180 @@ ServerConnector.prototype.buildModel = function(remoteModel) {
remoteModel.methods.forEach(Model.defineMethod.bind(Model));
}
function Context(base, meta, model, method, args) {
/**
* Create a new model instance for the given data
* @param {String} model The model name
* @param {Object} data The model data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.create = function (model, data, callback) {
this.requestModel(model, {
method: 'POST',
body: data
}, callback);
};
/**
* Save the model instance for the given data
* @param {String} model The model name
* @param {Object} data The model data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.save = function (model, data, callback) {
var idValue = this.getIdValue(model, data);
if(idValue) {
this.requestModel(model, {
method: 'PUT',
body: data
}, callback);
} else {
this.create(model, data, callback);
}
};
/**
* Check if a model instance exists by id
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.exists = function (model, id, callback) {
this.requestModel(model, {
url: '/exists'
}, callback);
};
/**
* Find a model instance by id
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.find = function find(model, id, callback) {
this.requestModelById(model, id, {
ignoreNotFound: true
}, callback);
};
/**
* Update if the model instance exists with the same id or create a new instance
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
var self = this;
var idValue = self.getIdValue(model, data);
if (idValue === null || idValue === undefined) {
return this.create(data, callback);
}
this.find(model, idValue, function (err, inst) {
if (err) {
return callback(err);
}
if (inst) {
self.updateAttributes(model, idValue, data, callback);
} else {
self.create(model, data, function (err, id) {
if (err) {
return callback(err);
}
if (id) {
self.setIdValue(model, data, id);
callback(null, data);
} else {
callback(null, null); // wtf?
}
});
}
});
};
/**
* Delete a model instance by id
* @param {String} model The model name
* @param {*} id The id value
* @param [callback] The callback function
*/
ServerConnector.prototype.destroy = function destroy(model, id, callback) {
this.requestModelById(model, id, {
method: 'DELETE'
}, callback);
};
/**
* Find matching model instances by the filter
*
* @param {String} model The model name
* @param {Object} filter The filter
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.all = function all(model, filter, callback) {
this.requestModel(model, {
query: {filter: filter}
}, callback);
};
/**
* Delete all instances for the given model
* @param {String} model The model name
* @param {Object} [where] The filter for where
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.destroyAll = function destroyAll(model, where, callback) {
this.requestModel(model, {
method: 'DELETE',
query: {where: where}
}, callback);
};
/**
* Count the number of instances for the given model
*
* @param {String} model The model name
* @param {Function} [callback] The callback function
* @param {Object} filter The filter for where
*
*/
ServerConnector.prototype.count = function count(model, callback, where) {
this.requestModel(model, {
url: '/count',
query: {where: where}
}, callback);
};
/**
* Update properties for the model instance data
* @param {String} model The model name
* @param {Object} data The model data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.updateAttributes = function updateAttrs(model, id, data, callback) {
this.requestModelById(model, id, {
method: 'PUT',
url: '/updateAttributes'
}, callback);
};
function Context(base, meta, model, method, args, callback) {
this.base = base;
this.meta = meta;
this.model = model;
this.method = method;
this.args = this.mapArgs(args);
this.callback = callback;
}
/**
@ -99,11 +314,18 @@ Context.prototype.toRequest = function() {
query: this.query(),
method: this.verb(),
body: this.body(),
headers: this.headers()
headers: this.headers(),
json: this.isJSON()
}
}
Context.prototype.isJSON = function() {
return true;
}
Context.prototype.url = function() {
var ctx = this;
var args = this.args;
var url = path.join(
this.base,
this.meta.baseRoute.path,
@ -111,6 +333,13 @@ Context.prototype.url = function() {
);
// replace url fragments with url params
this.method.accepts.forEach(function(param) {
var argName = param.arg;
var val = args[argName];
if(param && param.http && param.http.source === 'path') {
url = url.replace(':' + argName, val);
}
});
return url;
}
@ -203,3 +432,13 @@ Context.prototype.mapArgs = function(args) {
return result;
}
Context.prototype.handleResponse = function(err, res, body) {
// TODO handle `returns` correctly
this.callback.call(this, err, body);
}
Context.prototype.invoke = function() {
var req = this.toRequest();
request(req, this.handleResponse.bind(this));
}

View File

@ -321,6 +321,7 @@ loopback.RoleMapping = require('./models/role').RoleMapping;
loopback.ACL = require('./models/acl').ACL;
loopback.Scope = require('./models/acl').Scope;
loopback.Change = require('./models/change');
loopback.Checkpoint = require('./models/checkpoint');
/*!
* Automatically attach these models to dataSources

View File

@ -1,44 +0,0 @@
/*!
* Module dependencies.
*/
var loopback = require('../loopback');
/**
* Export the middleware.
*/
module.exports = models;
/**
* Return a script that defines all remote models.
*/
function models(app) {
return function (req, res, next) {
var script = 'window.loopback.remoteModels = ';
var models = [];
app.handler('rest').adapter.getClasses().forEach(function(c) {
if (!c.ctor) {
// Skip classes that don't have a shared ctor
// as they are not LoopBack models
console.error('Skipping %j as it is not a LoopBack model', name);
return;
}
models.push(toJSON(c));
});
res.send(script + JSON.stringify(models, null, 2));
}
}
function toJSON(sharedClass) {
var model = loopback.getModel(sharedClass.name);
return {
modelName: model.modelName,
settings: model.settings,
properties: model.properties,
baseRoute: sharedClass.routes[0],
methods: sharedClass.methods
};
}

View File

@ -27,7 +27,8 @@ var properties = {
*/
var options = {
trackChanges: false
trackChanges: false,
strict: true
};
/**
@ -316,9 +317,12 @@ Change.prototype.isBasedOn = function(change) {
Change.diff = function(modelName, since, remoteChanges, callback) {
var remoteChangeIndex = {};
var modelIds = [];
remoteChanges.forEach(function(ch) {
var Change = this;
remoteChanges.map(function(ch) {
ch = new Change(ch);
modelIds.push(ch.modelId);
remoteChangeIndex[ch.modelId] = new Change(ch);
remoteChangeIndex[ch.modelId] = ch;
return ch;
});
// normalize `since`
@ -336,8 +340,11 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
var localModelIds = [];
localChanges.forEach(function(localChange) {
localChange = new Change(localChange);
localModelIds.push(localChange.modelId);
var remoteChange = remoteChangeIndex[localChange.modelId];
if(!remoteChange) return;
if(!localChange.equals(remoteChange)) {
if(remoteChange.isBasedOn(localChange)) {
deltas.push(remoteChange);

View File

@ -307,7 +307,7 @@ Model.changes = function(since, filter, callback) {
checkpoint: {gt: since},
modelName: this.modelName
}, function(err, changes) {
if(err) return cb(err);
if(err) return callback(err);
var ids = changes.map(function(change) {
return change.modelId.toString();
});
@ -490,7 +490,6 @@ Model.bulkUpdate = function(updates, callback) {
// tasks.push(model.save.bind(model));
tasks.push(function(cb) {
var model = new Model(update.data);
debugger;
model.save(cb);
});
break;
@ -513,9 +512,10 @@ Model.bulkUpdate = function(updates, callback) {
*/
Model.getChangeModel = function() {
var changeModel = this.Change;
var changeModelName = this.modelName + '-change';
var changeModel = this.Change || loopback.getModel(changeModelName);
if(changeModel) return changeModel;
this.Change = changeModel = require('./change').extend(this.modelName + '-change');
this.Change = changeModel = require('./change').extend(changeModelName);
changeModel.attachTo(this.dataSource);
return changeModel;
}

View File

@ -31,7 +31,8 @@
"underscore": "~1.5.2",
"uid2": "0.0.3",
"async": "~0.2.9",
"canonical-json": "0.0.3"
"canonical-json": "0.0.3",
"browser-request": "~0.3.1"
},
"peerDependencies": {
"loopback-datasource-juggler": "~1.2.13"