UNPKG

apostrophe

Version:

The Apostrophe Content Management System.

376 lines (345 loc) • 12.1 kB
// This module allows other modules to create command line tasks. // // A command line task is invoked like this: // // node app apostrophe-migrations:migrate // // Apostrophe is fully initialized before your task is run, except that it does // not listen for connections. So you may access all of its features in your task. /* eslint-disable no-console */ var _ = require('@sailshq/lodash'); var Promise = require('bluebird'); module.exports = { alias: 'tasks', singletonWarningIfNot: 'apostrophe-tasks', afterConstruct: function(self) { self.apos.on('runTask', self.run); }, construct: function(self, options) { self.tasks = {}; // For use when you wish to execute an Apostrophe command line task from your code and continue, // without using the command line or using the `child_process` module. // // Except for `name`, all arguments may be omitted. // // If you do not pass a callback, a promise is returned. // // Examples (assume `products` extends `apostrophe-pieces`): // // `self.apos.tasks.invoke('apostrophe-users:add', [ 'admin', 'admin' ]).then(function() { ... })` // // `self.apos.tasks.invoke('products:generate', { total: 20 }).then(function() { ... })` // // The `args` and `options` arguments may be completely omitted. // // If present, `args` contains an array of positional arguments to // the task, **not including** the task name. // // If present, `options` contains the optional parameters that would normally // be hyphenated, i.e. at the command line you might write `--total=20`. // // **Gotchas** // // If you can invoke a method directly rather than invoking a task, do that. This // method is for cases where that option is not readily available. // // During the execution of the task, `self.apos.argv` will have a new, // temporary value to accommodate tasks that inspect this property directly // rather than examining their `argv` argument. `self.apos.argv` will be // restored at the end of task execution. // // Some tasks may not be written to be "good neighbors." For instance, a // task developer might assume they can exit the process directly. self.invoke = function(name, args, options, callback) { const aposArgv = self.apos.argv; if (Array.isArray(args)) { args.splice(0, 0, name); } else { callback = options; options = args; args = [ name ]; } if (!((typeof options) === 'object')) { callback = options; options = {}; } if (!callback) { return Promise.promisify(body)(); } else { return body(callback); } function body(callback) { const task = self.find(name); const argv = { _: args }; Object.assign(argv, options || {}); self.apos.argv = argv; var promise = task.callback(self.apos, argv, after); if (promise && promise.then) { return promise.then(function() { return after(null); }).catch(after); } function after(err) { self.apos.argv = aposArgv; return callback(err); } } }; // Add a command line task to Apostrophe. It is easiest to invoke this // via the `addTask` method of your own module. You may also call it // directly. // // If you do call directly, the group name should be the name of your module. // // The name may be any short, memorable identifier, hyphenated if necessary. // // You may omit the `usage` parameter complete if you don't want to supply // a help message, but we recommend that you do so. // // Your callback function receives `(apos, argv, callback)`. Your // function should perform the necessary task, referring to // `argv._` for positional command line arguments (`[0]` is the task name) // and to `argv.foo` for an option specified as `--foo=bar`. // // On completing the task your function should invoke the callback. // If the callback is invoked with `null`, Apostrophe will exit quietly // with status 0 (success), otherwise it will display the error given // and exit with status 1 (failure). // // **Your task may return a promise** instead of invoking the callback. // You **must not** do both. // // Your code will usually need to invoke methods that require a `req` argument. // Call `self.apos.tasks.getReq()` to get a `req` object with // unlimited admin permissions. Use `self.apos.tasks.getAnonReq()` to get // a `req` object without permissions. self.add = function(groupName, name, usage, callback) { if (arguments.length === 3) { callback = usage; usage = undefined; } if (!_.has(self.tasks, groupName)) { self.tasks[groupName] = {}; } self.tasks[groupName][name] = { callback: callback, usage: usage }; }; // You should not need to call this method directly. You probably // want `apos.tasks.invoke` (see above). // // This method is invoked by Apostrophe to execute the task specified // by the first command line argument. On completion the process exits. // If the task experiences an error it is printed to `console.error` // and the process exits with a nonzero status code. // // This method also implements the `help` task directly. self.run = function() { var task; var cmd = self.apos.argv._[0]; if (!cmd) { throw new Error('apos.tasks.run invoked but there is no command line argument to serve as a task name, should never happen'); } if (cmd === 'help') { // list all tasks if (self.apos.argv._.length === 1) { self.usage(); } // help with specific task if (self.apos.argv._.length === 2) { task = self.find(self.apos.argv._[1]); if (!task) { console.error('There is no such task.'); self.usage(); } if (task.usage) { console.log('\nTips for the ' + task.fullName + ' task:\n'); console.log(task.usage); } else { console.log('That is a valid task, but it does not have a help message.'); } process.exit(0); } } task = self.find(cmd); if (!task) { console.error('\nThere is no such task.'); self.usage(); } var promise = task.callback(self.apos, self.apos.argv, afterTask); if (promise && promise.then) { return promise.then(function() { return afterTask(null); }).catch(afterTask); } function afterTask(err) { if (err) { console.error(err); process.exit(1); } process.exit(0); } }; // Identifies the task corresponding to the given command line argument. // This allows for Rails-style hyphenated syntax with a `:` separator, // which is the documented syntax, and also allows camel-cased syntax with a `.` // separator for those who prefer a more JavaScript-y syntax. self.find = function(fullName) { var matches = fullName.match(/^(.*?):(.*)$/); if (!matches) { return false; } else { var groupName = matches[1]; var name = matches[2]; if (!_.has(self.tasks, groupName)) { return false; } if (!_.has(self.tasks[groupName], name)) { return false; } } var task = self.tasks[groupName][name]; task.fullName = groupName + ':' + name; return task; }; // Displays a usage message, including a list of available tasks, // and exits the entire program with a nonzero status code. self.usage = function() { // Direct use of console makes sense in tasks. -Tom console.error('\nThe following tasks are available:\n'); _.each(self.tasks, function(group, groupName) { _.each(group, function(task, name) { console.error(groupName + ':' + name); }); }); console.error('\nType:\n'); console.error('node app help groupname:taskname\n'); console.error('To get help with a specific task.\n'); console.error('To launch the site, run with no arguments.'); process.exit(1); }; // Return a `req` object with permission to do anything. // Useful since most APIs require one and most tasks // should run with administrative rights. // // The `req` object returned is a mockup of a true Express `req` object // with sufficient functionality to implement Apostrophe's // unit tests, so it is suitable for command line // task code that requires a `req` as well. // // Optionally a `properties` object can be passed. If it is // passed its properties are added to the req object before // any initialization tasks such as computing `req.absoluteUrl`. // This allows testing of that mechanism by setting `req.url`. self.getReq = function(properties) { var req = { user: { _permissions: { admin: true } }, res: { __ns: function(namespace, s) { return s; }, __ns_n: function(namespace, s) { return s; }, __ns_mf: function(namespace, s) { return s; }, __ns_l: function(namespace, s) { return s; }, __ns_h: function(namespace, s) { return s; }, __: function(s) { return s; }, __n: function(s) { return s; }, __mf: function(s) { return s; }, __l: function(s) { return s; }, __h: function(s) { return s; } }, __ns: function(namespace, s) { return s; }, __ns_n: function(namespace, s) { return s; }, __ns_mf: function(namespace, s) { return s; }, __ns_l: function(namespace, s) { return s; }, __ns_h: function(namespace, s) { return s; }, __: function(s) { return s; }, __n: function(s) { return s; }, __mf: function(s) { return s; }, __l: function(s) { return s; }, __h: function(s) { return s; }, data: {}, protocol: 'http', get: function(propName) { return { Host: 'you-need-to-set-baseUrl-in-app-js.com' }[propName]; }, browserCall: self.apos.app && self.apos.app.request.browserCall, getBrowserCalls: self.apos.app && self.apos.app.request.getBrowserCalls, query: {}, url: '/', session: {} }; req.res.req = req; _.extend(req, properties || {}); self.apos.modules['apostrophe-express'].addAbsoluteUrlsToReq(req); return req; }; // Return a `req` object with privileges equivalent // to an anonymous user visiting the website. Most // often used for unit testing but sometimes useful // in tasks as well. // // The `req` object returned is a mockup of a true Express `req` object // with sufficient functionality to implement Apostrophe's // unit tests, so it is suitable for command line // task code that requires a `req` as well. // // Optionally a `properties` object can be passed. If it is // passed its properties are added to the req object before // any initialization tasks such as computing `req.absoluteUrl`. // This allows testing of that mechanism by setting `req.url`. self.getAnonReq = function(properties) { var req = self.getReq(); delete req.user; _.extend(req, properties || {}); return req; }; } };