From 4466c54921cfdfb5fa68e80f22dbe9200dbb4a18 Mon Sep 17 00:00:00 2001 From: ryedin Date: Tue, 6 May 2014 20:50:34 -0500 Subject: [PATCH 1/5] added ability for app to load things asynchronously; added tests and documentation; --- Gruntfile.js | 32 +++++- README.md | 193 +++++++++++++++++++++++++++++++++++++ tasks/loopback_angular.js | 35 +++++-- test/fixtures/app-async.js | 38 ++++++++ 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/app-async.js diff --git a/Gruntfile.js b/Gruntfile.js index 83d9140..b66acda 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -64,7 +64,37 @@ module.exports = function(grunt) { // Whenever the "test" task is run, first clean the "tmp" dir, then run this // plugin's task(s), then test the result. - grunt.registerTask('test', ['clean', 'loopback_angular', 'nodeunit']); + grunt.registerTask('test:sync', ['clean', 'loopback_angular', 'nodeunit']); + + // need a special config and fixture to test the async case + grunt.registerTask( + 'test:async', + 'Configure the default options for the async test.', + function() { + + grunt.config.set('loopback_angular', { + options: { + input: 'test/fixtures/app-async.js', + appReady: 'onReady' + }, + default_options: { + options: { + output: 'tmp/default_options' + } + }, + custom_options: { + options: { + output: 'tmp/custom_options', + ngModuleName: 'customServices', + apiUrl: 'http://custom/api/' + } + } + }); + + grunt.task.run('clean', 'loopback_angular', 'nodeunit'); + }); + + grunt.registerTask('test', ['test:sync', 'test:async']); // By default, lint and run all tests. grunt.registerTask('default', ['jshint', 'test']); diff --git a/README.md b/README.md index 7eb69c6..3bb9906 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,199 @@ See the official [LoopBack AngularJS SDK documentation](http://docs.strongloop.com/display/DOC/AngularJS+JavaScript+SDK#AngularJSJavaScriptSDK-Gruntplugin) for the description of tasks provided by this Grunt plugin. +## Async loading of LoopBack App + +If your LoopBack app (i.e. the `input` option for this grunt task) needs to perform asynchronous operations before this task runs (like fetch a set of metadata that you want to introspect to build your models), you must export and specify an `appReady` method that registers one or more callback functions and calls them when it is done building/configuring LoopBack. + +EXAMPLE: your `app.js` might look something like this... + +``` +var loopback = require('loopback'), + path = require('path'), + app = module.exports = loopback(), + started = new Date(); + +//create an 'appReady' callback handler so code that exports this file can know when the full loopback service is configured + +var onReadyCallbacks = []; +app.onReady = function(cb) { + onReadyCallbacks.push(cb); +}; + +function doOnReady() { + _.each(onReadyCallbacks, function(cb) { + cb && cb(); + }); +} + +//do something asyncronously that gets me some model metadata I can use later +var myAsyncThing = require('my-async-model-building-module'); + +myAsyncThing.run(function(metadata) { + + /* + * 1. Configure LoopBack models and datasources + * + * Read more at http://apidocs.strongloop.com/loopback#appbootoptions + */ + + app.boot(__dirname); + + //use the model metadata to introspectively register new models + myAsyncThing.buildModels(metadata); + + /* + * 2. Configure request preprocessing + * + * LoopBack support all express-compatible middleware. + */ + + app.use(loopback.favicon()); + app.use(loopback.logger(app.get('env') === 'development' ? 'dev' : 'default')); + + app.use(loopback.bodyParser()); + app.use(loopback.methodOverride()); + + /* + * EXTENSION POINT + * Add your custom request-preprocessing middleware here. + * Example: + * app.use(loopback.limit('5.5mb')) + */ + + /* + * 3. Setup request handlers. + */ + + // LoopBack REST interface + var apiPath = '/api'; + app.use(loopback.cookieParser('secret')); + app.use(loopback.token({model: app.models.accessToken})); + app.use(apiPath, loopback.rest()); + + // API explorer (if present) + var explorerPath = '/explorer'; + try { + var explorer = require('loopback-explorer'); + app.use(explorerPath, explorer(app, { basePath: apiPath })); + } catch(e){ + // ignore errors, explorer stays disabled + } + + /* + * EXTENSION POINT + * Add your custom request-handling middleware here. + * Example: + * app.use(function(req, resp, next) { + * if (req.url == '/status') { + * // send status response + * } else { + * next(); + * } + * }); + */ + + // 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. + app.use(app.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. + app.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. + app.use(loopback.urlNotFound()); + + /* + * 4. Setup error handling strategy + */ + + /* + * EXTENSION POINT + * Add your custom error reporting middleware here + * Example: + * app.use(function(err, req, resp, next) { + * console.log(req.url, ' failed: ', err.stack); + * next(err); + * }); + */ + + // The ultimate error handler. + app.use(loopback.errorHandler()); + + + /* + * 5. Add a basic application status route at the root `/`. + * + * (remove this to handle `/` on your own) + */ + + // app.get('/', loopback.status()); + + app.enableAuth(); + + //app API is ready to go -- time to alert anyone who cares (lookin' at you grunt-loopback-angular) + doOnReady(); + + if(require.main === module) { + require('http').createServer(app).listen(app.get('port'), app.get('host'), function(){ + var baseUrl = 'http://' + app.get('host') + ':' + app.get('port'); + if (explorerPath) { + console.log('Browse your REST API at %s%s', baseUrl, explorerPath); + } else { + console.log( + 'Run `npm install loopback-explorer` to enable the LoopBack explorer'); + } + console.log('LoopBack server listening @ %s%s', baseUrl, '/'); + }); + } +}); + +``` + +And then your Gruntfile would look like this… + +``` +module.exports = function(grunt) { + grunt.initConfig({ + loopback_angular: { + services: { + options: { + input: './app.js', + appReady: 'onReady', + output: '../client/app/scripts/lb-services.js' + } + } + }, + docular: { + groups: [ + { + groupTitle: 'LoopBack', + groupId: 'loopback', + sections: [ + { + id: 'lbServices', + title: 'LoopBack Services', + scripts: [ '../client/app/scripts/lb-services.js' ] + } + ] + } + ] + } + }); + // Load the plugin that provides the "loopback-angular" and "grunt-docular" tasks. + grunt.loadNpmTasks('grunt-loopback-angular'); + grunt.loadNpmTasks('grunt-docular'); + // Default task(s). + grunt.registerTask('default', ['loopback_angular', 'docular']); +}; +``` + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/tasks/loopback_angular.js b/tasks/loopback_angular.js index c8a6d77..1d66ac1 100644 --- a/tasks/loopback_angular.js +++ b/tasks/loopback_angular.js @@ -24,6 +24,9 @@ module.exports = function(grunt) { function runTask() { /*jshint validthis:true */ + //need this to accomodate an input file that builds the loopback interface asynchronously + var done = this.async(); + // Merge task-specific and/or target-specific options with these defaults. var options = this.options({ ngModuleName: 'lbServices', @@ -50,16 +53,34 @@ module.exports = function(grunt) { grunt.fail.warn(err); } - options.apiUrl = options.apiUrl || app.get('restApiRoot') || '/api'; + var completeTask = function() { + options.apiUrl = options.apiUrl || app.get('restApiRoot') || '/api'; - grunt.log.writeln('Generating %j for the API endpoint %j', - options.ngModuleName, - options.apiUrl); + grunt.log.writeln('Generating %j for the API endpoint %j', + options.ngModuleName, + options.apiUrl); - var script = generator.services(app, options.ngModuleName, options.apiUrl); + var script = generator.services(app, options.ngModuleName, options.apiUrl); - grunt.file.write(options.output, script); + grunt.file.write(options.output, script); - grunt.log.ok('Generated Angular services file %j', options.output); + grunt.log.ok('Generated Angular services file %j', options.output); + + done(); + }; + + var appReady = options.appReady, + appReadyMethod = app[appReady]; + + if (appReady) { + if (!appReadyMethod) { + grunt.fail.warn('An appReady method was specified but not found on the exported input object.'); + } + + //register with the app's ready callback handler + appReadyMethod(completeTask); + } else { + completeTask(); + } } }; diff --git a/test/fixtures/app-async.js b/test/fixtures/app-async.js new file mode 100644 index 0000000..349beea --- /dev/null +++ b/test/fixtures/app-async.js @@ -0,0 +1,38 @@ +var loopback = require('loopback'); +var app = loopback(); + +module.exports = app; + +//setup an onReady handler +var onReady; +app.onReady = function(cb) { + + //to get the grunt multitask to work for this test, we need to also + //handle the case of the non-first iterations. In those cases, this file + //will have already been required so the outer process.nextTick below will not + //happen again, and neither will the on ready below. Therefore, if onReady is already + //defined, just directly simulate async... + if (onReady) { + process.nextTick(onReady); + } + + //still overwrite each time with the new callback + onReady = cb; +}; + +//simulate the case where there are async operations that the app does before it's ready to have its lb-services generated +process.nextTick(function() { + + // Setup default datasources for autoAttach() + app.dataSource('db', { connector: 'memory', defaultForType: 'db' }); + app.dataSource('mail', { connector: 'mail', defaultForType: 'mail' }); + + // Attach all built-in models + loopback.autoAttach(); + + // Configure REST API path + app.set('restApiRoot', '/rest-api-root'); + + //execute onReady handler if it was registered + onReady && onReady(); +}); \ No newline at end of file From df1295cb518196466b34a56adc540a42aca44733 Mon Sep 17 00:00:00 2001 From: ryedin Date: Tue, 6 May 2014 20:58:04 -0500 Subject: [PATCH 2/5] clarified the test:async grunt command description --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index b66acda..ca9fb2a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -69,7 +69,7 @@ module.exports = function(grunt) { // need a special config and fixture to test the async case grunt.registerTask( 'test:async', - 'Configure the default options for the async test.', + 'Configure the fixture and options for the async test and run it.', function() { grunt.config.set('loopback_angular', { From f19d45f9d00631ec63c861a5b796d0d9fd869d68 Mon Sep 17 00:00:00 2001 From: ryedin Date: Tue, 6 May 2014 21:02:34 -0500 Subject: [PATCH 3/5] imports, not exports --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bb9906..6ec59b6 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ var loopback = require('loopback'), app = module.exports = loopback(), started = new Date(); -//create an 'appReady' callback handler so code that exports this file can know when the full loopback service is configured +//create an 'appReady' callback handler so code that imports this file can know when the full loopback service is configured var onReadyCallbacks = []; app.onReady = function(cb) { From 57aad3253a954ef655e498043b9be18ef9c61a3c Mon Sep 17 00:00:00 2001 From: ryedin Date: Wed, 7 May 2014 07:51:17 -0500 Subject: [PATCH 4/5] require underscore in README example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ec59b6..4fce2eb 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ If your LoopBack app (i.e. the `input` option for this grunt task) needs to perf EXAMPLE: your `app.js` might look something like this... ``` -var loopback = require('loopback'), +var _ = require('underscore'), + loopback = require('loopback'), path = require('path'), app = module.exports = loopback(), started = new Date(); From 4eb63edc841a538c5c302aa834c8de3dd370b466 Mon Sep 17 00:00:00 2001 From: ryedin Date: Fri, 9 May 2014 21:07:51 -0500 Subject: [PATCH 5/5] change to event rather than callback registration function; docs; tests; --- Gruntfile.js | 21 ++-- README.md | 138 ++-------------------- tasks/loopback_angular.js | 16 +-- test/async/loopback_angular_async_test.js | 43 +++++++ test/fixtures/app-async.js | 21 +--- 5 files changed, 67 insertions(+), 172 deletions(-) create mode 100644 test/async/loopback_angular_async_test.js diff --git a/Gruntfile.js b/Gruntfile.js index ca9fb2a..1aee4f5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,24 +73,19 @@ module.exports = function(grunt) { function() { grunt.config.set('loopback_angular', { - options: { - input: 'test/fixtures/app-async.js', - appReady: 'onReady' - }, - default_options: { - options: { - output: 'tmp/default_options' - } - }, - custom_options: { + services: { options: { - output: 'tmp/custom_options', - ngModuleName: 'customServices', - apiUrl: 'http://custom/api/' + input: 'test/fixtures/app-async.js', + appReadyEvent: 'ready', + output: 'tmp/async_app' } } }); + grunt.config.set('nodeunit', { + tests: ['test/async/*_test.js'] + }); + grunt.task.run('clean', 'loopback_angular', 'nodeunit'); }); diff --git a/README.md b/README.md index 4fce2eb..b5b8786 100644 --- a/README.md +++ b/README.md @@ -36,150 +36,30 @@ If your LoopBack app (i.e. the `input` option for this grunt task) needs to perf EXAMPLE: your `app.js` might look something like this... ``` -var _ = require('underscore'), - loopback = require('loopback'), - path = require('path'), - app = module.exports = loopback(), - started = new Date(); - -//create an 'appReady' callback handler so code that imports this file can know when the full loopback service is configured - -var onReadyCallbacks = []; -app.onReady = function(cb) { - onReadyCallbacks.push(cb); -}; - -function doOnReady() { - _.each(onReadyCallbacks, function(cb) { - cb && cb(); - }); -} - +var loopback = require('loopback'), + app = module.exports = loopback(); + //do something asyncronously that gets me some model metadata I can use later var myAsyncThing = require('my-async-model-building-module'); myAsyncThing.run(function(metadata) { - /* - * 1. Configure LoopBack models and datasources - * - * Read more at http://apidocs.strongloop.com/loopback#appbootoptions - */ - + // Configure LoopBack models and datasources app.boot(__dirname); //use the model metadata to introspectively register new models myAsyncThing.buildModels(metadata); - /* - * 2. Configure request preprocessing - * - * LoopBack support all express-compatible middleware. - */ - + // other configuration as scaffolded by `slc lb project` app.use(loopback.favicon()); - app.use(loopback.logger(app.get('env') === 'development' ? 'dev' : 'default')); - - app.use(loopback.bodyParser()); - app.use(loopback.methodOverride()); - - /* - * EXTENSION POINT - * Add your custom request-preprocessing middleware here. - * Example: - * app.use(loopback.limit('5.5mb')) - */ - - /* - * 3. Setup request handlers. - */ - - // LoopBack REST interface - var apiPath = '/api'; - app.use(loopback.cookieParser('secret')); - app.use(loopback.token({model: app.models.accessToken})); - app.use(apiPath, loopback.rest()); - - // API explorer (if present) - var explorerPath = '/explorer'; - try { - var explorer = require('loopback-explorer'); - app.use(explorerPath, explorer(app, { basePath: apiPath })); - } catch(e){ - // ignore errors, explorer stays disabled - } - - /* - * EXTENSION POINT - * Add your custom request-handling middleware here. - * Example: - * app.use(function(req, resp, next) { - * if (req.url == '/status') { - * // send status response - * } else { - * next(); - * } - * }); - */ - - // 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. - app.use(app.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. - app.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. - app.use(loopback.urlNotFound()); - - /* - * 4. Setup error handling strategy - */ - - /* - * EXTENSION POINT - * Add your custom error reporting middleware here - * Example: - * app.use(function(err, req, resp, next) { - * console.log(req.url, ' failed: ', err.stack); - * next(err); - * }); - */ - - // The ultimate error handler. - app.use(loopback.errorHandler()); - - - /* - * 5. Add a basic application status route at the root `/`. - * - * (remove this to handle `/` on your own) - */ - - // app.get('/', loopback.status()); - + // etc. app.enableAuth(); //app API is ready to go -- time to alert anyone who cares (lookin' at you grunt-loopback-angular) - doOnReady(); + app.emit('ready'); if(require.main === module) { - require('http').createServer(app).listen(app.get('port'), app.get('host'), function(){ - var baseUrl = 'http://' + app.get('host') + ':' + app.get('port'); - if (explorerPath) { - console.log('Browse your REST API at %s%s', baseUrl, explorerPath); - } else { - console.log( - 'Run `npm install loopback-explorer` to enable the LoopBack explorer'); - } - console.log('LoopBack server listening @ %s%s', baseUrl, '/'); - }); + //start the http server } }); @@ -194,7 +74,7 @@ module.exports = function(grunt) { services: { options: { input: './app.js', - appReady: 'onReady', + appReadyEvent: 'ready', output: '../client/app/scripts/lb-services.js' } } diff --git a/tasks/loopback_angular.js b/tasks/loopback_angular.js index 1d66ac1..7cd8498 100644 --- a/tasks/loopback_angular.js +++ b/tasks/loopback_angular.js @@ -24,7 +24,8 @@ module.exports = function(grunt) { function runTask() { /*jshint validthis:true */ - //need this to accomodate an input file that builds the loopback interface asynchronously + // need this to accomodate an input file that builds the loopback + // interface asynchronously var done = this.async(); // Merge task-specific and/or target-specific options with these defaults. @@ -69,16 +70,9 @@ module.exports = function(grunt) { done(); }; - var appReady = options.appReady, - appReadyMethod = app[appReady]; - - if (appReady) { - if (!appReadyMethod) { - grunt.fail.warn('An appReady method was specified but not found on the exported input object.'); - } - - //register with the app's ready callback handler - appReadyMethod(completeTask); + if (options.appReadyEvent) { + //register with the app's ready event + app.on(options.appReadyEvent, completeTask); } else { completeTask(); } diff --git a/test/async/loopback_angular_async_test.js b/test/async/loopback_angular_async_test.js new file mode 100644 index 0000000..b80d57b --- /dev/null +++ b/test/async/loopback_angular_async_test.js @@ -0,0 +1,43 @@ +'use strict'; + +var grunt = require('grunt'); +var parse = require('loopback-angular/parse-helper'); + +/* + ======== A Handy Little Nodeunit Reference ======== + https://github.com/caolan/nodeunit + + Test methods: + test.expect(numAssertions) + test.done() + Test assertions: + test.ok(value, [message]) + test.equal(actual, expected, [message]) + test.notEqual(actual, expected, [message]) + test.deepEqual(actual, expected, [message]) + test.notDeepEqual(actual, expected, [message]) + test.strictEqual(actual, expected, [message]) + test.notStrictEqual(actual, expected, [message]) + test.throws(block, [error], [message]) + test.doesNotThrow(block, [error], [message]) + test.ifError(value) +*/ + +// Note: the tests are depending on targets configured in Gruntfile.js + +exports.loopback_angular = { + setUp: function(done) { + // setup here if necessary + done(); + }, + + async_app: function(test) { + var script = grunt.file.read('tmp/async_app'); + + // the value "lbservices" is the --module-name default + test.equal(parse.moduleName(script), 'lbServices'); + // the value "/rest-api-root" is hard-coded in fixtures/app.js + test.equal(parse.baseUrl(script), '/rest-api-root'); + test.done(); + } +}; diff --git a/test/fixtures/app-async.js b/test/fixtures/app-async.js index 349beea..5c77b67 100644 --- a/test/fixtures/app-async.js +++ b/test/fixtures/app-async.js @@ -3,23 +3,6 @@ var app = loopback(); module.exports = app; -//setup an onReady handler -var onReady; -app.onReady = function(cb) { - - //to get the grunt multitask to work for this test, we need to also - //handle the case of the non-first iterations. In those cases, this file - //will have already been required so the outer process.nextTick below will not - //happen again, and neither will the on ready below. Therefore, if onReady is already - //defined, just directly simulate async... - if (onReady) { - process.nextTick(onReady); - } - - //still overwrite each time with the new callback - onReady = cb; -}; - //simulate the case where there are async operations that the app does before it's ready to have its lb-services generated process.nextTick(function() { @@ -33,6 +16,6 @@ process.nextTick(function() { // Configure REST API path app.set('restApiRoot', '/rest-api-root'); - //execute onReady handler if it was registered - onReady && onReady(); + //let anything that cares (require'ing code) know the app is ready + app.emit('ready'); }); \ No newline at end of file