1 | var _ = require('underscore')
|
2 | var fs = require('fs')
|
3 | var utils = require('./utils')
|
4 | var txain = require('txain')
|
5 | var path = require('path')
|
6 | var inquirer = require('inquirer')
|
7 | var multiline = require('multiline')
|
8 | var AWS = require('aws-sdk')
|
9 | require('colors')
|
10 | var temp = require('temp').track()
|
11 |
|
12 | var configurationKeys = ['timeout', 'memory_size', 'role', 'description']
|
13 |
|
14 | module.exports = function(yargs) {
|
15 | yargs.command('defaults', 'Handles default values for new controllers', function (yargs) {
|
16 |
|
17 | yargs.command('set', 'Sets the default value of a key for new controllers', function(yargs) {
|
18 | var questions = [
|
19 | {
|
20 | 'name': 'key',
|
21 | 'type': 'list',
|
22 | 'choices': configurationKeys,
|
23 | 'message': 'Choose the setting you want to set',
|
24 | },
|
25 | {
|
26 | 'name': 'value',
|
27 | 'message': 'Write the value for this setting',
|
28 | },
|
29 | ]
|
30 | inquirer.prompt(questions, function(answers) {
|
31 | var config = utils.readConfig()
|
32 | config.defaults = config.defaults || {}
|
33 | config.defaults.controllers = config.defaults.controllers || {}
|
34 | config.defaults.controllers[answers.key] = answers.value
|
35 | utils.writeConfig(config)
|
36 | console.log('Updated controllers defaults configuration successfully')
|
37 | })
|
38 | })
|
39 |
|
40 | yargs.command('get', 'Gets the current defaults values', function(yargs) {
|
41 | var config = utils.readConfig()
|
42 | config.defaults = config.defaults || {}
|
43 | config.defaults.controllers = config.defaults.controllers || {}
|
44 | configurationKeys.forEach(function(key) {
|
45 | console.log(key.white.bold)
|
46 | console.log(' ', config.defaults.controllers[key] || 'not defined')
|
47 | })
|
48 | })
|
49 |
|
50 | yargs.help('help')
|
51 | })
|
52 | .command('create', 'Creates a new controller and the required API resources to map it to a URL', function (yargs) {
|
53 | var questions = [
|
54 | {
|
55 | 'name': 'function',
|
56 | 'message': 'Function name',
|
57 | },
|
58 | {
|
59 | 'name': 'method',
|
60 | 'message': 'HTTP method',
|
61 | 'type': 'list',
|
62 | 'choices': ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS'],
|
63 | 'default': 'GET',
|
64 | },
|
65 | {
|
66 | 'name': 'path',
|
67 | 'message': 'HTTP path',
|
68 | 'default': '/',
|
69 | },
|
70 | {
|
71 | 'name': 'handler',
|
72 | 'message': 'JavaScript method name',
|
73 | 'default': 'run',
|
74 | },
|
75 | {
|
76 | 'name': 'input',
|
77 | 'message': 'HTTP Content-Type input type',
|
78 | 'type': 'list',
|
79 | 'choices': ['json', 'form'],
|
80 | 'default': 'json',
|
81 | },
|
82 | {
|
83 | 'name': 'output',
|
84 | 'message': 'HTTP Content-Type output type',
|
85 | 'type': 'list',
|
86 | 'choices': ['json', 'html'],
|
87 | 'default': 'json',
|
88 | },
|
89 | ]
|
90 |
|
91 | inquirer.prompt(questions, function(answers) {
|
92 | txain(function(callback) {
|
93 | var filename = path.join(process.cwd(), 'web/controllers/'+answers['function']+'.js')
|
94 | if (answers.output === 'html') {
|
95 | var code = multiline.stripIndent(function() {; |
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 | })
|
102 | } else {
|
103 | var code = multiline.stripIndent(function() {; |
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 | })
|
110 | }
|
111 | code = code.replace('HANDLER', answers.handler)
|
112 | fs.writeFile(filename, code, 'utf-8', callback)
|
113 | })
|
114 | .then(function(data, callback) {
|
115 | var routes = utils.readRoutes()
|
116 | routes.push(_.pick(answers, 'method', 'path', 'function', 'handler', 'input', 'output'))
|
117 | utils.writeRoutes(routes)
|
118 | callback()
|
119 | })
|
120 | .end(utils.end)
|
121 | })
|
122 | })
|
123 | .command('delete', 'Deletes a controller', function (yargs) {
|
124 | utils.notImplemented()
|
125 | })
|
126 | .command('set', 'Sets the value of a configuration key for an existing controller', function(yargs) {
|
127 | utils.notImplemented()
|
128 | })
|
129 | .command('sync', 'Synchronizes the controller with AWS. It creates or updates the lambda function and all the API resources if needed', function(yargs) {
|
130 | utils.requiresRegion()
|
131 | utils.requiresAPI()
|
132 |
|
133 | var choices = recentControllers().map(function(route) {
|
134 | return route['function']
|
135 | })
|
136 | var question = {
|
137 | 'name': 'function',
|
138 | 'message': 'Function to be synched',
|
139 | 'type': 'list',
|
140 | 'choices': choices,
|
141 | 'validate': function(foo) {
|
142 | return true
|
143 | }
|
144 | }
|
145 | inquirer.prompt([question], function(answers) {
|
146 | var functionName = answers['function']
|
147 | module.exports.sync(functionName, utils.end)
|
148 | })
|
149 | })
|
150 | .command('list', 'Lists the available controllers and their mappings', function(yargs) {
|
151 | var argv = yargs
|
152 | .help('help')
|
153 | .argv
|
154 | })
|
155 |
|
156 | .wrap(yargs.terminalWidth())
|
157 | .help('help')
|
158 | .argv
|
159 | }
|
160 |
|
161 | function recentControllers() {
|
162 | var routes = utils.readRoutes()
|
163 | routes.forEach(function(route) {
|
164 |
|
165 | route.stat = fs.statSync(path.join('web/controllers', route['function']+'.js'))
|
166 | })
|
167 | return routes.sort(function(a, b) {
|
168 | return a.stat.mtime < b.stat.mtime ? 1 : -1
|
169 | })
|
170 | }
|
171 |
|
172 | module.exports.sync = function(functionName, callback) {
|
173 | var region = utils.requiresRegion()
|
174 | var api = utils.requiresAPI()
|
175 | var routes = utils.readRoutes()
|
176 | var route = _.findWhere(routes, { 'function': functionName })
|
177 |
|
178 | if (!route) {
|
179 | console.log('No controller found with that function name')
|
180 | process.exit(1)
|
181 | }
|
182 |
|
183 | var config = utils.readConfig()
|
184 | config.defaults = config.defaults || {}
|
185 | config.defaults.controllers = config.defaults.controllers || {}
|
186 |
|
187 | var controller = _.extend({}, route, config.defaults.controllers)
|
188 |
|
189 | var lambda = new AWS.Lambda()
|
190 | var apigateway = new AWS.APIGateway()
|
191 |
|
192 | var parentResource, resourceId
|
193 |
|
194 | txain(function(callback) {
|
195 | var zipfile = temp.path({ suffix: '.zip' })
|
196 | var filename = path.join(process.cwd(), 'web/controllers/'+controller['function']+'.js')
|
197 | var code = fs.readFileSync(filename, 'utf-8')
|
198 | var writeStream = fs.createWriteStream(zipfile)
|
199 | var archiver = require('archiver')('zip')
|
200 | archiver.pipe(writeStream)
|
201 | archiver.append(code, { name: 'index.js' })
|
202 | archiver.finalize()
|
203 | writeStream.on('close', function() {
|
204 | fs.readFile(zipfile, callback)
|
205 | })
|
206 | })
|
207 | .then(function(data, callback) {
|
208 | if (!route.functionArn) {
|
209 | txain(function(callback) {
|
210 | var params = {
|
211 | Code: {
|
212 | ZipFile: data,
|
213 | },
|
214 | FunctionName: functionName,
|
215 | Handler: 'index.'+route.handler,
|
216 | Role: controller.role,
|
217 | Runtime: 'nodejs',
|
218 | Description: controller.description,
|
219 | MemorySize: controller.memorySize,
|
220 | Publish: false,
|
221 | Timeout: controller.timeout,
|
222 | }
|
223 | console.log('Creating lambda function')
|
224 | lambda.createFunction(params, callback)
|
225 | })
|
226 | .then(function(body, callback) {
|
227 | route.functionArn = body.FunctionArn
|
228 | utils.writeRoutes(routes)
|
229 |
|
230 | console.log('Adding permission to lambda function')
|
231 |
|
232 | var uuid = require('node-uuid')
|
233 | var params = {
|
234 | Action: 'lambda:InvokeFunction',
|
235 | FunctionName: functionName,
|
236 | Principal: 'apigateway.amazonaws.com',
|
237 | StatementId: uuid.v4(),
|
238 | }
|
239 | lambda.addPermission(params, callback)
|
240 | })
|
241 | .end(callback)
|
242 | } else {
|
243 | txain(function(callback) {
|
244 | var params = {
|
245 | ZipFile: data,
|
246 | FunctionName: functionName,
|
247 | Publish: false,
|
248 | }
|
249 | console.log('Updating lambda function code')
|
250 | lambda.updateFunctionCode(params, callback)
|
251 | })
|
252 | .then(function(callback) {
|
253 | var params = {
|
254 | FunctionName: functionName,
|
255 | Handler: 'index.handler',
|
256 | Role: controller.role,
|
257 | Description: controller.description,
|
258 | MemorySize: controller.memorySize,
|
259 | Timeout: controller.timeout,
|
260 | }
|
261 | console.log('Updating function configuration')
|
262 | lambda.updateFunctionConfiguration(params, callback)
|
263 | })
|
264 | .end(callback)
|
265 | }
|
266 | })
|
267 | .then(function(callback) {
|
268 | console.log('Creating API resources')
|
269 | apigateway.getResources({ restApiId: api.id }, callback)
|
270 | })
|
271 | .then(function(data, callback) {
|
272 | var resources = data.items
|
273 | resources.forEach(function(resource) {
|
274 | if (controller.path.indexOf(resource.path) === 0
|
275 | && (!parentResource || resource.path.length > parentResource.path.length)) {
|
276 | parentResource = resource
|
277 | }
|
278 | })
|
279 | var pathParts = _.compact(controller.path.substring(parentResource.path.length).split('/'))
|
280 | callback(null, pathParts)
|
281 | })
|
282 | .each(function(part, callback) {
|
283 | txain(function(callback) {
|
284 | var params = {
|
285 | restApiId: api.id,
|
286 | parentId: parentResource.id,
|
287 | pathPart: part,
|
288 | }
|
289 | apigateway.createResource(params, callback)
|
290 | })
|
291 | .then(function(body, callback) {
|
292 | parentResource = body
|
293 | callback()
|
294 | })
|
295 | .end(callback)
|
296 | })
|
297 | .then(function(callback) {
|
298 | resourceId = parentResource.id
|
299 | var method = parentResource.resourceMethods && parentResource.resourceMethods[controller.method]
|
300 |
|
301 | txain(function(callback) {
|
302 | if (!method) return callback()
|
303 | var params = {
|
304 | restApiId: api.id,
|
305 | resourceId: resourceId,
|
306 | httpMethod: controller.method,
|
307 | }
|
308 | apigateway.deleteMethod(params, callback)
|
309 | })
|
310 | .then(function(callback) {
|
311 | var params = {
|
312 | authorizationType: 'none',
|
313 | httpMethod: controller.method,
|
314 | resourceId: resourceId,
|
315 | restApiId: api.id,
|
316 | apiKeyRequired: false,
|
317 | }
|
318 | apigateway.putMethod(params, callback)
|
319 | })
|
320 | .end(callback)
|
321 | })
|
322 | .then(function(body, callback) {
|
323 |
|
324 | console.log('Creating integration')
|
325 | var params = {
|
326 | restApiId: api.id,
|
327 | resourceId: resourceId,
|
328 | httpMethod: controller.method,
|
329 | integrationHttpMethod: 'POST',
|
330 | type: 'AWS',
|
331 | uri: 'arn:aws:apigateway:'+AWS.config.region+':lambda:path/2015-03-31/functions/'+route.functionArn+'/invocations',
|
332 | requestTemplates: {
|
333 | 'application/json': multiline.stripIndent(function() {; |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 | })
|
356 | },
|
357 | }
|
358 | apigateway.putIntegration(params, callback)
|
359 | })
|
360 | .then(function(callback) {
|
361 | console.log('Creating integration response')
|
362 | var params = {
|
363 | httpMethod: controller.method,
|
364 | resourceId: resourceId,
|
365 | restApiId: api.id,
|
366 | statusCode: '200',
|
367 | responseParameters: {},
|
368 | responseTemplates: {},
|
369 | selectionPattern: '.*',
|
370 | }
|
371 | if (route.output === 'html') {
|
372 | params.responseTemplates['text/html'] = '$input.path(\'$.html\')'
|
373 | }
|
374 | apigateway.putIntegrationResponse(params, callback)
|
375 | })
|
376 | .then(function(callback) {
|
377 | console.log('Creating method response')
|
378 | var params = {
|
379 | httpMethod: controller.method,
|
380 | resourceId: resourceId,
|
381 | restApiId: api.id,
|
382 | statusCode: '200',
|
383 | responseModels: {},
|
384 | responseParameters: {},
|
385 | }
|
386 | if (route.output === 'html') {
|
387 | params.responseModels['text/html'] = 'Empty'
|
388 | }
|
389 | apigateway.putMethodResponse(params, callback)
|
390 | })
|
391 | .end(callback)
|
392 | }
|