UNPKG

lambdasync

Version:

Easy sync between local code and AWS lambda functions

270 lines (238 loc) 7.92 kB
const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const cp = require('child_process'); const marked = require('marked'); const TerminalRenderer = require('marked-terminal'); const spawn = require('cross-spawn'); const {LAMBDASYNC_SRC} = require('./constants'); const {readFile} = require('./file'); marked.setOptions({ // Define custom renderer renderer: new TerminalRenderer() }); // Executes a CLI command and returns a promise function promisedExec(command, options = {}) { // eslint-disable-line no-unused-vars return new Promise((resolve, reject) => { cp.exec(command, options, (err, stdout) => { if (err) { return reject(err); } resolve(stdout); }); }); } // Replaces {{vars}} in strings function mustacheLite(template, data = {}) { let content = template; Object.keys(data).forEach(key => { content = content.replace(new RegExp(`{{${key}}}`, 'g'), data[key]); }); return content; } // Takes a markdown string, or path to a markdown file (relative to Lambdasync's `src` dir) // and produces terminal styled markdown // Will also replace an mustahce vars with values from the supplied data object function markdown({templateString = null, templatePath = null, data = {}}) { const template = templateString ? templateString : fs.readFileSync(path.join(LAMBDASYNC_SRC, templatePath), 'utf8'); const content = mustacheLite(template, data); const md = marked(content); return `\n${md}\n`; } // Takes an object of {key,label} and a data object and produces // markdown for a bold label and inline code ticks around the value // that was fetched from the data object using the key function markdownProperty({key, label}, data) { if (data && Object.prototype.hasOwnProperty.call(data, key)) { return '**' + label + ':** `' + data[key] + '`\n'; } return ''; } // Helps add default values to `inquirer` prompt objects function addInputDefault(defaults, inputConfig) { if (defaults[inputConfig.name]) { return Object.assign({}, inputConfig, {default: defaults[inputConfig.name]}); } return inputConfig; } // Calls an aws sdk class and method and returns a promise function awsPromise(api, method, params) { return new Promise((resolve, reject) => { try { api[method](params, (err, data) => { if (err) { return reject(err); } return resolve(data); }); } catch (err) { return reject(err) } }); } // Removes the `:12345` version at the end of the function ARN function stripLambdaVersion(lambdaArn) { return lambdaArn.replace(/:[0-9]+$/, ''); } function makeLambdaPolicyArn({lambdaArn, apiGatewayId}) { return lambdaArn .replace('arn:aws:lambda', 'arn:aws:execute-api') .replace(/function.*?$/g, apiGatewayId) .concat(`/*/*/*`); } // takes an array of CLI args [ 'timeout=10' ] and returns a key value object // {timeout: 10}, it will also try to JSON parse args function parseCommandArgs(args = [], settings = {}) { return args.reduce((acc, current) => { let [key, valueKey] = current.split('='); if (!key || !valueKey) { return acc; } // If string starts with a [ or {, JSON.parse it if (valueKey[0] === '[' || valueKey[0] === '{') { try { valueKey = JSON.parse(valueKey); } catch (err) {} } acc[key] = settings[valueKey] || valueKey; return acc; }, {}); } function functionExists(api, functionName) { return new Promise((resolve, reject) => { const params = { FunctionName: functionName }; api.getFunction(params, err => { if (err) { if (err.toString().includes('ResourceNotFoundException')) { return resolve(false); } return reject(err); } return resolve(true); }); }); } function copyPackageJson(templateDir, targetDir, data) { const jsonTemplate = fs.readFileSync(path.join(templateDir, 'package.json'), 'utf8'); return fs.writeFileSync( path.join(targetDir, 'package.json'), mustacheLite(jsonTemplate, data) ); } function hashPackageDependencies({dependencies = {}}) { if (!dependencies) { return null; } return crypto.createHash('md5').update(JSON.stringify(dependencies)).digest('hex'); } const logger = label => input => { console.log('\n\n' + label + '\n'); console.log(input); console.log('\n\n'); return input; }; function handleGenericFailure() { // TODO: Log errors here, possibly to a Lambda instance? :) console.log(markdown({ templatePath: 'markdown/generic-fail.md' })); } const logMessage = message => input => { console.log(message); return input; }; function formatTimestamp(timestamp) { // Timestamp is in UTC, but user wants to see local time so add the offset // Inverse the offset since we have a UTC time to convert to local const offset = new Date().getTimezoneOffset() * -1; const localTime = new Date(timestamp.getTime() + (offset * 60 * 1000)); if (isDate(localTime)) { const dateStr = localTime.toISOString(); return dateStr.replace('T', ' ').substring(0, dateStr.indexOf('.')); } return null; } const delay = time => input => new Promise(resolve => { setTimeout(() => { resolve(input); }, time); }); const startWith = data => Promise.resolve(data); function npmInstall(flags = '') { return new Promise((resolve, reject) => { var child = spawn('npm', ['install', flags], {stdio: 'inherit'}); child.on('close', code => { if (code !== 0) { return reject('npm install failed'); } return resolve(); }); }); } function ignoreData() { return {}; } function isDate(date) { return Object.prototype.toString.call(date) === '[object Date]' && (date.toString() && date.toString() !== 'Invalid Date'); } function removeCurrentPath(path = '') { const pathToRemove = `${process.cwd()}/`; return path.replace(pathToRemove, ''); } function removeFileExtension(path = '') { // Instead of setting rules for what a file extension is based on length and allowed chars // Let's just specify which file endings we want to be able to remove const extensionsToRemove = ['js']; // Get position of last dot const lastDotPosition = path.lastIndexOf('.'); // If none are found we can safely just return the original string if (lastDotPosition === -1) { return path; } // Get the file extension (right of the last dot) const maybeExtension = path.substr(lastDotPosition + 1); if (extensionsToRemove.indexOf(maybeExtension) !== -1) { return path.substr(0, lastDotPosition); } return path; } function makeAbsolutePath(inPath) { // First find out if the path is absolute if (path.isAbsolute(inPath)) { return inPath; } // Otherwise build an absolute path from process.cwd() return path.join(process.cwd(), inPath); } exports = module.exports = {}; exports.promisedExec = promisedExec; exports.handleGenericFailure = handleGenericFailure; exports.markdown = markdown; exports.markdownProperty = markdownProperty; exports.mustacheLite = mustacheLite; exports.addInputDefault = addInputDefault; exports.awsPromise = awsPromise; exports.stripLambdaVersion = stripLambdaVersion; exports.startWith = startWith; exports.delay = delay; exports.makeLambdaPolicyArn = makeLambdaPolicyArn; exports.parseCommandArgs = parseCommandArgs; exports.logger = logger; exports.logMessage = logMessage; exports.formatTimestamp = formatTimestamp; exports.isDate = isDate; exports.functionExists = functionExists; exports.copyPackageJson = copyPackageJson; exports.npmInstall = npmInstall; exports.hashPackageDependencies = hashPackageDependencies; exports.ignoreData = ignoreData; exports.removeFileExtension = removeFileExtension; exports.makeAbsolutePath = makeAbsolutePath; exports.removeCurrentPath = removeCurrentPath; if (process.env.NODE_ENV === 'test') { exports.isDate = isDate; }