diff --git a/api/app.api.js b/api/app.api.js index ce43545..5395f5d 100644 --- a/api/app.api.js +++ b/api/app.api.js @@ -3,29 +3,22 @@ var path = require('path'); var loopback = require('loopback'); var explorer = require('loopback-explorer'); var CONFIG = require('global.config'); -var LOCAL_CONFIG = require('local.config'); +var boot = require('loopback-boot'); // server var server = module.exports = loopback(); -// data source -// var db = loopback.createDataSource(LOCAL_CONFIG.db); -var db = loopback.createDataSource({ - connector: require('loopback-connector-mongodb'), - database: 'todo-example' +boot(server, { + appRootDir: path.resolve(__dirname), + modelsRootDir: path.resolve(__dirname, '..'), + appConfig: { + // No app config to load, we are processing it manually now + // TODO(bajtos) modify Gulp to create app.* config files + }, }); -// models -var User = require('models/user'); -var Todo = require('models/todo'); - -// setup the model data sources -User.attachTo(db); -Todo.attachTo(db); -server.model(User); -server.model(Todo); - // TODO(ritch) this should be unecessary soon.... +var Todo = server.models.Todo; server.model(Todo.getChangeModel()); // root api path diff --git a/api/configure.js b/api/configure.js index 3ea2a2c..2a4e2b0 100644 --- a/api/configure.js +++ b/api/configure.js @@ -6,17 +6,3 @@ exports.global = function(env, global) { root: '/api' }; } - -exports.local = function(env, global, local) { - var db = local.db = {}; - - if(env === 'staging' || env === 'production') { - db.host = process.env.DB_HOST || global.api.host; - db.port = process.env.DB_PORT || 27015; - db.connector = 'mongodb'; - db.user = process.env.DB_USER; - db.password = process.env.DB_PASSWORD; - } else { - db.connector = 'memory'; - } -} diff --git a/api/datasources.json b/api/datasources.json new file mode 100644 index 0000000..ee53ada --- /dev/null +++ b/api/datasources.json @@ -0,0 +1,6 @@ +{ + "db": { + "connector": "memory", + "defaultForType": "db" + } +} diff --git a/api/datasources.production.js b/api/datasources.production.js new file mode 100644 index 0000000..e1680e1 --- /dev/null +++ b/api/datasources.production.js @@ -0,0 +1,2 @@ +// Use the same environment-based configuration as in staging +module.exports = require('./datasources.staging'); diff --git a/api/datasources.staging.js b/api/datasources.staging.js new file mode 100644 index 0000000..2d40ca7 --- /dev/null +++ b/api/datasources.staging.js @@ -0,0 +1,12 @@ +var globalConfig = require('global.config'); + +module.exports = { + db: { + connector: 'mongodb', + hostname: process.env.DB_HOST || globalConfig.api.host, + port: process.env.DB_PORT || 27017, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: 'todo-example', + } +}; diff --git a/gulpfile.js b/gulpfile.js index 9468d11..2c62981 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -67,8 +67,10 @@ function findAndBuild(packageName, cb) { function findSrc(package) { var paths = []; findPackages(package).forEach(function(pkg) { - paths.push(path.join(__dirname, pkg, '*', '*') + '.js'); + paths.push(path.join(__dirname, pkg, '*', '*.js')); + paths.push(path.join(__dirname, pkg, '*', '*.json')); paths.push(path.join(__dirname, pkg, '*.js')); + paths.push(path.join(__dirname, pkg, '*.json')); }); return paths; } diff --git a/html5/app.html5.js b/html5/app.html5.js index c22d5c8..b78ae97 100644 --- a/html5/app.html5.js +++ b/html5/app.html5.js @@ -2,26 +2,41 @@ var LOCAL_CONFIG = require('local.config'); var loopback = require('loopback'); var client = exports.client = loopback(); -var async = require('async'); // angular.js dependencies require('./bower_components/angular/angular.js'); require('./bower_components/angular-route/angular-route.js'); -// data sources -var remote = loopback.createDataSource({ - connector: loopback.Remote, - url: LOCAL_CONFIG.serverInfo.url -}); -var memory = loopback.createDataSource({ - connector: loopback.Memory, - localStorage: 'todo-db' -}); - -// models -var User = require('models/user'); -var RemoteTodo = window.RemoteTodo = require('models/todo'); -var LocalTodo = window.LocalTodo = RemoteTodo.extend('LocalTodo'); +var lbapp = loopback(); + +require('./build/datasources.js')(lbapp); +require('./build/models.js')(lbapp); + +// TODO(bajtos) Move the bi-di replication to loopback core, +// add model settings to enable the replication. +// Example: +// LocalTodo: { options: { +// base: 'Todo', +// replicate: { +// target: 'Todo', +// mode: 'push' | 'pull' | 'bidi' +// }}} + +var LocalTodo = lbapp.models.LocalTodo; +var RemoteTodo = lbapp.models.Todo; + +// setup model replication +function sync(cb) { + if(window.connected()) { + RemoteTodo.replicate(LocalTodo, function() { + LocalTodo.replicate(RemoteTodo, cb); + }); + } +} + +// sync local changes if connected +LocalTodo.on('changed', sync); +LocalTodo.on('deleted', sync); // routes var routes = LOCAL_CONFIG.routes; @@ -47,11 +62,6 @@ require('./controllers/login.ctrl'); require('./controllers/register.ctrl'); require('./controllers/change.ctrl'); -// setup the model data sources -RemoteTodo.attachTo(remote); -LocalTodo.attachTo(memory); - - window.isConnected = true; window.connected = function connected() { @@ -59,19 +69,6 @@ window.connected = function connected() { return window.isConnected; } -// setup model replication -function sync(cb) { - if(connected()) { - RemoteTodo.replicate(LocalTodo, function() { - LocalTodo.replicate(RemoteTodo, cb); - }); - } -} - -// sync local changes if connected -LocalTodo.on('changed', sync); -LocalTodo.on('deleted', sync); - // setup routes Object.keys(routes) .forEach(function(route) { diff --git a/html5/client.models.json b/html5/client.models.json new file mode 100644 index 0000000..226454e --- /dev/null +++ b/html5/client.models.json @@ -0,0 +1,8 @@ +{ + "LocalTodo": { + "dataSource": "local", + "options": { + "base": "Todo" + } + } +} diff --git a/html5/configure.js b/html5/configure.js index 6b53f2c..37f89f5 100644 --- a/html5/configure.js +++ b/html5/configure.js @@ -1,9 +1,11 @@ var gulp = require('gulp'); +var async = require('async'); var path = require('path'); var pkg = require('./package.json'); var fs = require('fs'); var browserify = require('browserify'); var sh = require('shelljs'); +var BootConfigLoader = require('loopback-boot').ConfigLoader; exports.global = function(env, global) { // routes @@ -61,6 +63,105 @@ exports.local = function configure(env, global, local) { } exports.build = function(env, global, local, cb) { + async.waterfall([ + function(next) { + async.parallel([ + function(next) { + buildDataSources(env, next); + }, + function(next) { + buildModels(env, next); + } + ], function(err) { next(err); }); + }, + function(next) { + createBundle(env, global, next); + } + ], cb); +}; + +function buildDataSources(env, cb) { + var dataSources = BootConfigLoader.loadDataSources(__dirname, env); + for (var name in dataSources) { + var cfg = dataSources[name]; + var connector = cfg.connector; + if (connector !== 'memory' && connector !== 'remote') { + return cb(new Error( + 'Datasource ' + name + ' uses unknown connector ' + connector + '.')); + } + } + + var dsconfig = 'var loopback = require(\'loopback\');' + + '\nvar config;' + + '\nmodule.exports = function(app) {\n' + + ' for (var name in config) {\n' + + ' app.dataSource(name, config[name]);\n' + + ' }\n'+ + '};\n' + + '\n' + + 'config = ' + + JSON.stringify(dataSources, null, 2) + + ';\n'; + + fs.writeFile( + path.resolve(__dirname, 'build', 'datasources.js'), + dsconfig, + 'utf-8', + cb); +} + +function buildModels(env, cb) { + var models = BootConfigLoader.loadModels(path.resolve(__dirname, '..'), env); + var clientModels = require('./client.models.json'); + + var modelsFile = path.resolve(__dirname, 'build', 'models.js'); + var modelsDir = path.resolve(__dirname, '..', 'models'); + + fs.readdir(modelsDir, function(err, files) { + if (err) return cb(err); + + var code = 'var loopback = require(\'loopback\');\n' + + 'var models, clientModels;\n\n' + + 'module.exports = function(app) {\n' + + ' for (var name in models) {\n' + + ' app.model(name, models[name]);\n' + + ' }\n\n'; + + files.forEach(function(f) { + f = path.resolve(modelsDir, f); + if (path.extname(f) !== '.js' || !fs.statSync(f).isFile()) return; + var relative = path.relative(path.dirname(modelsFile), f); + code += ' runExportedFn(app, require(' + + JSON.stringify(relative) + '));\n'; + }); + + // It is important to create client models only after the shared models + // were fully initialized. Otherwise client models extending server models + // won't get static methods defined in models/*.js scripts. + code += '\n' + + ' for (name in clientModels) {\n' + + ' app.model(name, clientModels[name]);\n' + + ' }\n'; + + //TODO(bajtos) Require and run all files in `client.models/*` + + // end of the exported fn + code += '};\n'; + + code += '\nmodels = ' + JSON.stringify(models, null, 2) + ';\n'; + code += '\nclientModels = ' + JSON.stringify(clientModels, null, 2) + ';\n'; + code += '\nfunction runExportedFn(app, fn) {\n' + + ' var executable = typeof fn === \'function\' &&\n' + + ' !(fn.prototype instanceof loopback.Model);\n' + + ' if (!executable) return;\n' + + ' fn(app);\n' + + '}'; + + fs.writeFile(modelsFile, code, 'utf-8', cb); + }); +} + +function createBundle(env, global, cb) { var b = browserify({basedir: __dirname}); b.add('./' + pkg.main); @@ -76,7 +177,11 @@ exports.build = function(env, global, local, cb) { }, 'uglifyify'); } - b.bundle().pipe(out); + b.bundle({ + // TODO(bajtos) debug should be always true, the sourcemaps should be + // saved to a standalone file when !isDev(env) + debug: isDev(env) + }).pipe(out); out.on('error', cb); out.on('close', cb); diff --git a/html5/datasources.json b/html5/datasources.json new file mode 100644 index 0000000..5ef7a4a --- /dev/null +++ b/html5/datasources.json @@ -0,0 +1,9 @@ +{ + "db": { + "connector": "remote" + }, + "local": { + "connector": "memory", + "localStorage": "todo-db" + } +} diff --git a/html5/datasources.local.js b/html5/datasources.local.js new file mode 100644 index 0000000..b578bc0 --- /dev/null +++ b/html5/datasources.local.js @@ -0,0 +1,7 @@ +var LOCAL_CONFIG = require('local.config'); + +module.exports = { + db: { + url: LOCAL_CONFIG.serverInfo.url + } +}; diff --git a/models.json b/models.json new file mode 100644 index 0000000..0a6fd45 --- /dev/null +++ b/models.json @@ -0,0 +1,37 @@ +{ + "Todo": { + "options": { + "trackChanges": true, + "base": "DataModel" + }, + "properties": { + "id": { + "id": true, + "type": "string" + }, + "title": "string", + "completed": { + "type": "boolean", + "default": false + }, + "created": { + "type": "number", + "//default": "//TODO: Date.now" + } + }, + "dataSource": "db" + }, + "user": { + "options": { + "base": "User" + }, + "properties": { + "title": "string", + "done": { + "type": "boolean", + "default": false + } + }, + "dataSource": "db" + } +} diff --git a/models/index.js b/models/index.js deleted file mode 100644 index 096a804..0000000 --- a/models/index.js +++ /dev/null @@ -1,2 +0,0 @@ -exports.Todo = require('./todo'); -exports.User = require('./user'); diff --git a/models/package.json b/models/package.json deleted file mode 100644 index 9cd65fb..0000000 --- a/models/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "models", - "version": "0.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC" -} diff --git a/models/todo.js b/models/todo.js index 2c908ee..298d181 100644 --- a/models/todo.js +++ b/models/todo.js @@ -1,50 +1,45 @@ var loopback = require('loopback'); var async = require('async'); -var Todo = module.exports = loopback.DataModel.extend('Todo', { - id: {id: true, type: String}, - title: String, - completed: {type: Boolean, default: false}, - created: {type: Number, default: Date.now} -}, { - trackChanges: true -}); +module.exports = function(app) { + var Todo = app.models.Todo; -Todo.beforeSave = function(next, model) { - if(!model.id) model.id = 't-' + Math.floor(Math.random() * 10000).toString(); - next(); -} + Todo.beforeSave = function(next, model) { + if (!model.id) model.id = 't-' + Math.floor(Math.random() * 10000).toString(); + next(); + }; -Todo.stats = function(filter, cb) { - var stats = {}; - cb = arguments[arguments.length - 1]; - var Todo = this; - - async.parallel([ - countComplete, - count - ], function(err) { - if(err) return cb(err); - stats.remaining = stats.total - stats.completed; - cb(null, stats); - }); + Todo.stats = function(filter, cb) { + var stats = {}; + cb = arguments[arguments.length - 1]; + var Todo = this; - function countComplete(cb) { - Todo.count({completed: true}, function(err, count) { - stats.completed = count; - cb(err); + async.parallel([ + countComplete, + count + ], function(err) { + if (err) return cb(err); + stats.remaining = stats.total - stats.completed; + cb(null, stats); }); - } - function count(cb) { - Todo.count(function(err, count) { - stats.total = count; - cb(err); - }); - } -} + function countComplete(cb) { + Todo.count({completed: true}, function(err, count) { + stats.completed = count; + cb(err); + }); + } -loopback.remoteMethod(Todo.stats, { - accepts: {arg: 'filter', type: 'object'}, - returns: {arg: 'stats', type: 'object'} -}); + function count(cb) { + Todo.count(function(err, count) { + stats.total = count; + cb(err); + }); + } + }; + + loopback.remoteMethod(Todo.stats, { + accepts: {arg: 'filter', type: 'object'}, + returns: {arg: 'stats', type: 'object'} + }); +}; diff --git a/models/user.js b/models/user.js deleted file mode 100644 index b7a2eae..0000000 --- a/models/user.js +++ /dev/null @@ -1,6 +0,0 @@ -var loopback = require('loopback'); - -var User = module.exports = loopback.User.extend('User', { - title: String, - done: {type: Boolean, default: false} -}); diff --git a/package.json b/package.json index 7ec9d10..5381488 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,11 @@ }, "dependencies": { "async": "~0.7.0", - "loopback": "2.0.0-beta2", + "loopback": "2.0.0-beta3", "loopback-explorer": "~1.1.0", "loopback-connector-mongodb": "^1.2.4", "loopback-datasource-juggler": "2.0.0-beta1", + "loopback-boot": "strongloop/loopback-boot", "ejs": "^1.0.0" } }