├── .babelrc ├── .editorconfig ├── .github └── workflows │ └── code-scanning-v3.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── index.ts ├── lambdas │ ├── aws-sts-caller-identity │ │ └── index.ts │ ├── func1 │ │ └── index.ts │ ├── func2 │ │ └── index.ts │ └── func3 │ │ └── index.ts ├── logging │ └── index.ts └── util │ ├── endsWith.js │ └── index.ts ├── test └── unit │ ├── lambda-aws-sts-caller-identity.test.ts │ ├── lambda-func1.test.ts │ ├── lambda-func2.test.ts │ ├── util.test.ts │ └── util │ └── invokeLambdaHandler.ts ├── tools └── bin │ └── zip-lambdas ├── tsconfig-src.json ├── tsconfig-test.json ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "6.10" 8 | }, 9 | "modules": false 10 | } 11 | ] 12 | ], 13 | "env": { 14 | "test": { 15 | "plugins": [ "istanbul" ], 16 | "sourceMaps": "inline" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{js,ts,json,yml}] 11 | charset = utf-8 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [Makefile] 16 | charset = utf-8 17 | indent_style = tab 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/workflows/code-scanning-v3.yml: -------------------------------------------------------------------------------- 1 | # This workflow is inherited from our internal .github repo at https://github.com/lifeomic/.github/blob/master/workflow-templates/code-scanning-v3.yml 2 | # 3 | # Setting up this workflow on the repository will perform a static scan for security issues using GitHub Code Scanning. 4 | # Any findings for a repository can be found under the `Security` tab -> `Code Scanning Alerts` 5 | name: "CodeQL" 6 | 7 | on: 8 | push: 9 | branches: [master] 10 | paths-ignore: 11 | - test 12 | - tests 13 | - '**/test' 14 | - '**/tests' 15 | - '**/*.test.js' 16 | - '**/*.test.ts' 17 | pull_request: 18 | branches: [master] 19 | paths-ignore: 20 | - test 21 | - tests 22 | - '**/test' 23 | - '**/tests' 24 | - '**/*.test.js' 25 | - '**/*.test.ts' 26 | 27 | jobs: 28 | analyze: 29 | name: Analyze 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | fail-fast: false 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v2 38 | with: 39 | # We must fetch at least the immediate parents so that if this is 40 | # a pull request then we can checkout the head. 41 | fetch-depth: 2 42 | 43 | # If this run was triggered by a pull request event, then checkout 44 | # the head of the pull request instead of the merge commit. 45 | - run: git checkout HEAD^2 46 | if: ${{ github.event_name == 'pull_request' }} 47 | 48 | # Initializes the CodeQL tools for scanning. 49 | - name: Initialize CodeQL 50 | uses: github/codeql-action/init@v1 51 | with: 52 | config-file: lifeomic/.github/config-files/codeql-config.yml@master # uses our config file from the lifeomic/.github repo 53 | queries: +security-extended # This will run all queries at https://github.com/github/codeql/:language/ql/src/codeql-suites/:language-security-extended.qls 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v1 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | #- run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v1 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | work/ 4 | .nyc_output/ 5 | /package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - yarn test 6 | - yarn bundle 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Phillip Gates-Idem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lambda + TypeScript + WebPack + Babel starter project 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/lifeomic/lambda-typescript-webpack-babel-starter.svg)](https://greenkeeper.io/) 4 | 5 | [![Build Status](https://travis-ci.org/lifeomic/lambda-typescript-webpack-babel-starter.svg?branch=master)](https://travis-ci.org/lifeomic/lambda-typescript-webpack-babel-starter) 6 | 7 | This project demonstrates using the following technologies: 8 | 9 | - [AWS Lambda](https://aws.amazon.com/lambda/): AWS Lambda allows developers 10 | to deploy packages of executable JavaScript code to the AWS infrastructure 11 | and make it executable without having to worry about managing servers. 12 | 13 | - [webpack](https://webpack.js.org/): `webpack` is used to create 14 | optimized bundles from JavaScript code that leverage ES6 modules. 15 | 16 | - [babel](https://babeljs.io/): `babel` transpiles JavaScript code to 17 | JavaScript code that is compatible with various runtimes. 18 | 19 | - [TypeScript](https://www.typescriptlang.org/): TypeScript is a typed 20 | superset of JavaScript that compiles to plain JavaScript. 21 | 22 | - [bunyan](https://github.com/trentm/node-bunyan): `bunyan` provides 23 | structured JSON logging. 24 | 25 | - [ava](https://github.com/avajs/ava): `ava` is a test runner. 26 | 27 | - [tslint](https://github.com/palantir/tslint): `tslint` is a TypeScript linter. 28 | 29 | - [yarn](https://yarnpkg.com/): `yarn` is a dependency manager that is an 30 | alternative to `npm`. 31 | 32 | - [nyc](https://github.com/istanbuljs/nyc): `nyc` provides a command-line 33 | interface for calculating test code coverage with 34 | [istanbul](https://github.com/istanbuljs/istanbuljs). 35 | 36 | - [chalk](https://github.com/chalk/chalk): `chalk` is used to add color to 37 | console output. 38 | 39 | - [proxyquire](https://github.com/thlorenz/proxyquire): `proxyquire` is used 40 | to substitute mock modules at runtime when running tests. 41 | 42 | ## Usage 43 | 44 | Clone this repo and make changes as you see fit. Push your changes 45 | to your own repo. 46 | 47 | ## Goals 48 | 49 | - Allow developers to use TypeScript for lambda functions, tests, and 50 | other source code. 51 | 52 | - Also allow JavaScript files to be used (hopefully sparingly). 53 | 54 | - Automatically compile JavaScript code related to AWS Lambda functions 55 | down to level supported by `node` `6.10` (which is currently the latest 56 | Node.js runtime that AWS lambda supports). 57 | 58 | - Calculate test code coverage using `nyc` when running tests using `ava`. 59 | 60 | - Create a bundle using `webpack` for each AWS Lambda function located at 61 | `src/lambdas/*`. 62 | 63 | ## Non-goals 64 | 65 | - Create a server and relaunch when file changes (this is not necessary 66 | due to the nature of testing and deploying lambda functions). 67 | 68 | - Provide a starter project that works for everyone. 69 | 70 | - Provide deployment scripts (use something like [terraform](https://www.terraform.io/docs/providers/aws/r/lambda_function.html), 71 | [CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html), or [aws-cli](https://docs.aws.amazon.com/cli/latest/reference/lambda/) if you need to deploy lambda functions). 72 | 73 | ## Scripts 74 | 75 | - `yarn test`: Use this command to lint, compile `test/**/*` files with 76 | TypeScript, and run tests with `ava` and `nyc`. We use the following 77 | glob pattern for unit tests: `test/**/*.test.js`. 78 | 79 | - `yarn build`: Use this command to compile the lambda functions. 80 | 81 | - `yarn bundle`: Use this command to build the lambda functions and then 82 | create zip files at `work/dist/*.zip`. 83 | 84 | - `yarn lint`: Use this command to lint the `src/**/*` and `test/**/*` 85 | files with `tslint` (does not require compilation). 86 | 87 | ## Why do some module paths start with `~/`? 88 | 89 | This starter project uses [require-self-ref](https://github.com/patrick-steele-idem/require-self-ref) 90 | to make it possible to `require`/`import` relative to the root of the project. 91 | 92 | For example, `import { sleep } from '~/src/util'` will always import 93 | the `sleep` function from `src/util/index.ts` no matter which file 94 | the import file is found in. 95 | 96 | In the `webpack` configuration we provide an `resolve.alias` property 97 | that automatically resolves `~/*` paths relative to the root of the project. 98 | 99 | In the `tsc` (TypeScript compiler) configuration, we use a combination of 100 | the `compilerOptions.baseUrl` and `compilerOptions.paths` properties to 101 | configure how `~/*` paths are resolved. 102 | 103 | At runtime, all test files include `import 'require-self-ref'` which 104 | loads the `require-self-ref` module which tweaks the Node.js module 105 | loader so that `~/*` paths are properly resolved at runtime. It's 106 | not necessary to use `require-self-ref` for lambda functions because 107 | the `webpack` bundling job will automatically inline all modules into 108 | a single bundle. 109 | 110 | ## TypeScript (compiler) 111 | 112 | [TypeScript](https://www.typescriptlang.org/) was chosen because it helps 113 | developers write code with more compile-time safety checks via it's 114 | flexible and powerful static typing. 115 | 116 | ### TypeScript Configuration 117 | 118 | This project provides multiple TypeScript configuration files and their 119 | usage is explained below. 120 | 121 | - `tsconfig.json`: This TypeScript configuration file is used by IDEs such 122 | as Visual Studio Code and Atom Editor. The `includes` for this configuration 123 | include `src/**/*` and `test/**/*`. 124 | 125 | The _compilation_ scripts in this project **do not** use this configuration 126 | when compiling files because the output settings for `test` and `src` 127 | are different. 128 | 129 | - `tsconfig-test.json`: This TypeScript configuration file is used to 130 | compile files in `test/` directory and the `module` output setting is 131 | `commonjs` so that they can be executed directly by Node.js runtime 132 | without having to be bundled or transpiled with `webpack` and `babel`. 133 | 134 | - `tsconfig-src.json`: This TypeScript configuration file is used to 135 | compile files in `src/` directory and the `module` output setting is 136 | `ES6` and the output files cannot be executed directly by Node.js. 137 | It is assumed that `webpack` and `babel` will be used to create a 138 | JavaScript bundle that targets the Node.js runtime supported by 139 | AWS Lambda. 140 | 141 | ## WebPack (bundling) 142 | 143 | The [webpack](https://webpack.js.org/) tool is used to create an 144 | optimized bundle for each lambda function located at `src/lambdas/`. 145 | 146 | The `webpack` tool is invoked via `yarn webpack` which cause `webpack` to 147 | use the default configuration located at `webpack.config.json`. 148 | 149 | This project **does not** use `webpack` to process code in the `test/*` 150 | directory when running tests. The test code is only compiled by 151 | the TypeScript compiler (`tsc`). The source files in `test/*` are 152 | compiled from TypeScript down to `commonjs` modules so that they can be 153 | loaded directly by the Node.js runtime. 154 | 155 | **NOTE:** In earlier versions of this starter project, we tried to use 156 | `rollup` but `rollup` poorly handled transforming JavaScript files 157 | that had both ES6 `import`/`export` statements and CommonJS `require(...)` 158 | statements in the same file. 159 | 160 | In our use case, we wanted to be able to `require(...)` normal JavaScript files 161 | and use `import`/`export` for ES6 modules created from TypeScript compiler. 162 | That is, in our use case a single JavaScript file compiled from TypeScript 163 | file might have a mix of `import`/`export` and `require(...)` statements. 164 | When the [rollup-plugin-commonjs](https://github.com/rollup/rollup-plugin-commonjs) 165 | plugin saw `import`/`export` in a file then it didn't bother traversing 166 | the `require(...)` statements (because it assumed that you don't mix 167 | both in the same file). This caused code referenced in these `require(...)` 168 | statements to not be bundled properly. 169 | 170 | ### WebPack Configuration 171 | 172 | The webpack configuration for each lambda function is dynamically produced 173 | inside `webpack.config.js`. `webpack` will automatically create a bundle as 174 | described by the `entry` property. The `entry` property value is an object in 175 | which each key is a bundle name and each associated value is the entry point 176 | file. 177 | 178 | ## Babel (JavaScript to JavaScript transpiler) 179 | 180 | The `babel` transpiler is integrated with `webpack` via `babel-loader` plugin. 181 | This project is only configured to use `babel` when creating the AWS lambda 182 | bundle files because we need to transpile all JavaScript files so that they 183 | are compatible with the Node.js runtime supported by AWS Lambda. Currently, 184 | the highest Node.js version supported bye AWS Lambda is `node` `6.10`. 185 | 186 | ### Babel Configuration 187 | 188 | The `babel` configuration is embedded inside the `webpack` configuration which 189 | allows flexibility in the future for having multiple `babel` configurations 190 | for the various output targets. 191 | 192 | The `babel-preset-env` package provides presets for `babel` that can be 193 | used to target specific runtime environments (for example, `node` `6.10`). 194 | 195 | The `babel` configuration is provided via the `options` property of the 196 | `babel-loader` plugin in `webpack.config.js` and it looks like the following: 197 | 198 | ```javascript 199 | { 200 | presets: [ 201 | [ 202 | 'env', 203 | { 204 | // Latest Node.js runtime for AWS Lambda functions is currently 6.10 205 | targets: { 206 | node: '6.10' 207 | }, 208 | modules: false 209 | } 210 | ] 211 | ], 212 | plugins: [ 213 | 'external-helpers' 214 | ] 215 | } 216 | ``` 217 | 218 | ## Bunyan (logging) 219 | 220 | This project uses `bunyan` because it uses structured JSON logging. 221 | 222 | ### Bunyan Configuration 223 | 224 | Bunyan loggers are created and configured when they are created via 225 | the `createLogger` function of `src/logging/index.ts`. 226 | 227 | ## Yarn Package Manager 228 | 229 | This project provides a `yarn.lock` file and uses `yarn` as its package manager. 230 | If you prefer `npm` then delete the `yarn.lock` file and run `npm install` 231 | which will create a `package-lock.json` file. 232 | 233 | ## License 234 | 235 | All source code for this project is provided under the MIT License. 236 | 237 | See `LICENSE` file. 238 | 239 | ## Contributing 240 | 241 | If you see ways to improve this project then please create a Pull Request 242 | or an Issue. 243 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "precompile-src": "rm -rf work/dist", 4 | "compile-src": "tsc -p tsconfig-src.json ", 5 | "postcompile-src": "cp package.json work/dist", 6 | "compile-test-ts": "tsc -p tsconfig-test.json", 7 | "transpile-test-js": "BABEL_ENV=test babel work/dist-test --out-dir work/dist-test --source-maps", 8 | "precompile-test": "rm -rf work/dist-test", 9 | "compile-test": "yarn compile-test-ts && yarn transpile-test-js", 10 | "postcompile-test": "cp package.json work/dist-test", 11 | "build": "yarn compile-src && yarn webpack --mode production", 12 | "bundle": "yarn build && ./tools/bin/zip-lambdas", 13 | "lint": "tslint --format codeFrame --project tsconfig.json 'src/**/*.ts' 'test/**/*.ts'", 14 | "pretest": "yarn lint && yarn compile-test", 15 | "test": "nyc ava 'work/dist-test/test/unit/**/*.test.js'" 16 | }, 17 | "devDependencies": { 18 | "@types/aws-lambda": "^8.10.0", 19 | "@types/bunyan": "^1.8.4", 20 | "@types/node": "^10.0.7", 21 | "@types/proxyquire": "^1.3.28", 22 | "ava": "^0.25.0", 23 | "babel-cli": "^6.26.0", 24 | "babel-core": "^6.26.0", 25 | "babel-loader": "^7.1.3", 26 | "babel-plugin-istanbul": "^4.1.5", 27 | "babel-preset-env": "^1.6.1", 28 | "babel-register": "^6.26.0", 29 | "builtin-modules": "^3.0.0", 30 | "chalk": "^2.3.2", 31 | "nyc": "^12.0.1", 32 | "proxyquire": "^2.0.0", 33 | "require-self-ref": "^2.0.1", 34 | "source-map-support": "^0.5.3", 35 | "tslint": "^5.9.1", 36 | "tslint-config-semistandard": "^7.0.0", 37 | "typescript": "^2.7.2", 38 | "webpack": "^4.1.0", 39 | "webpack-cli": "^3.0.0" 40 | }, 41 | "dependencies": { 42 | "aws-sdk": "^2.205.0", 43 | "bunyan": "^1.8.12" 44 | }, 45 | "ava": { 46 | "require": [ 47 | "require-self-ref", 48 | "source-map-support/register" 49 | ] 50 | }, 51 | "nyc": { 52 | "sourceMap": false, 53 | "instrument": false 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from '~/src/util'; 2 | 3 | async function run () { 4 | console.log('Going to sleep...'); 5 | await sleep(1000); 6 | console.log('Finished sleeping.'); 7 | } 8 | 9 | run().then(() => { 10 | console.log('DONE'); 11 | }).catch((err) => { 12 | console.error('ERROR:', err.stack || err.toString()); 13 | }); 14 | -------------------------------------------------------------------------------- /src/lambdas/aws-sts-caller-identity/index.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '~/src/logging'; 2 | import { STS } from 'aws-sdk'; 3 | 4 | const logger = createLogger(module); 5 | 6 | async function run (event: any, context: AWSLambda.Context) { 7 | const sts = new STS(); 8 | return sts.getCallerIdentity({}).promise(); 9 | } 10 | 11 | export function handler (event: any, context: AWSLambda.Context, callback: AWSLambda.Callback) { 12 | run(event, context).then((result: any) => { 13 | callback(null, result); 14 | }).catch((err) => { 15 | logger.error(err, 'Error occurred'); 16 | callback(err); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/lambdas/func1/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is module exports a `handler` function which is the entry point 3 | * for each invocation of this lambda function. 4 | */ 5 | 6 | import { sleep } from '~/src/util'; 7 | import { createLogger } from '~/src/logging'; 8 | 9 | const logger = createLogger('xyz'); 10 | const SLEEP_DURATION_IN_MILLISECONDS = 1000; 11 | 12 | async function run (event: any, context: AWSLambda.Context) { 13 | logger.info({ 14 | SLEEP_DURATION_IN_MILLISECONDS 15 | }, `Waiting...`); 16 | await sleep(SLEEP_DURATION_IN_MILLISECONDS); 17 | logger.info(`Finished waiting.`); 18 | return { 19 | name: 'func1' 20 | }; 21 | } 22 | 23 | export function handler (event: any, context: AWSLambda.Context, callback: AWSLambda.Callback) { 24 | run(event, context).then((result) => { 25 | logger.info({ 26 | event 27 | }, `${result.name} finished`); 28 | callback(null, result); 29 | }).catch((err) => { 30 | logger.error(err, 'Error occurred'); 31 | callback(err); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/lambdas/func2/index.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '~/src/logging'; 2 | 3 | const logger = createLogger(module); 4 | const endsWith = require('../../util/endsWith'); 5 | 6 | async function run (event: any, context: AWSLambda.Context) { 7 | const message = (endsWith(event.command, 'world')) 8 | ? 'hello to world' 9 | : 'hello to something else'; 10 | return { 11 | name: 'func2', 12 | message 13 | }; 14 | } 15 | 16 | export function handler (event: any, context: AWSLambda.Context, callback: AWSLambda.Callback) { 17 | run(event, context).then((result) => { 18 | logger.info({ 19 | event 20 | }, `${result.name} finished`); 21 | callback(null, result); 22 | }).catch((err) => { 23 | logger.error(err, 'Error occurred'); 24 | callback(err); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/lambdas/func3/index.ts: -------------------------------------------------------------------------------- 1 | export function handler (event: any, context: AWSLambda.Context, callback: AWSLambda.Callback) { 2 | callback(null, { 3 | success: true 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/logging/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | const bunyan = require('bunyan'); 4 | const PROJECT_NAME = require('~/package.json').name; 5 | const BASEDIR = path.normalize(path.join(__dirname, '../..')); 6 | 7 | function getLoggerName (moduleOrFile: string | NodeModule): string { 8 | let moduleFilename; 9 | 10 | if (moduleOrFile) { 11 | if (moduleOrFile.constructor === String) { 12 | moduleFilename = moduleOrFile as string; 13 | } else { 14 | // NOTE: `webpack` module loader doesn't add a `filename` to `Module` 15 | // instance so we can't rely on that if this source file was bundled 16 | // via `webpack`. 17 | moduleFilename = (moduleOrFile as NodeModule).filename; 18 | } 19 | 20 | if (moduleFilename) { 21 | moduleFilename = moduleFilename.toString(); 22 | const pos = moduleFilename.indexOf(BASEDIR); 23 | if (pos !== -1) { 24 | moduleFilename = moduleFilename.substring(BASEDIR.length + 1); 25 | } 26 | } 27 | } 28 | 29 | let name = moduleFilename || BASEDIR; 30 | if (name === '/') { 31 | name = PROJECT_NAME; 32 | } 33 | 34 | return name; 35 | } 36 | 37 | export function createLogger (moduleOrFile: string | NodeModule, options?: any) { 38 | let serializers; 39 | 40 | if (options) { 41 | if (options.serializers) { 42 | serializers = Object.assign({}, bunyan.stdSerializers, options.serializers); 43 | } 44 | } 45 | 46 | return bunyan.createLogger({ 47 | level: 'info', 48 | name: getLoggerName(moduleOrFile), 49 | serializers: serializers || bunyan.stdSerializers 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/util/endsWith.js: -------------------------------------------------------------------------------- 1 | // NOTE: This file is plain JavaScript to demonstrate how JavaScript 2 | // and TypeScript can coexist. 3 | 4 | module.exports = (str, search) => { 5 | if (str == null) { 6 | return false; 7 | } 8 | 9 | const len = str.length; 10 | return str.substring(len - search.length, len) === search; 11 | }; 12 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export async function sleep (duration: number) { 2 | return new Promise((resolve, reject) => { 3 | setTimeout(resolve, duration); 4 | }); 5 | } 6 | 7 | export async function addAsync (num1: number, num2: number) { 8 | await sleep(0); 9 | return num1 + num2; 10 | } 11 | -------------------------------------------------------------------------------- /test/unit/lambda-aws-sts-caller-identity.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This unit test is used to test a lambda function that uses 3 | * the `aws-sdk`. In this starter project, the `aws-sts-caller-identy` 4 | * Lambda function uses the `STS` class provided by the `aws-sdk` to 5 | * get the 'caller identity'. Since our test is not actually given 6 | * AWS credentials, we create a mock for `aws-sdk` and use `proxyquire` 7 | * to replace the actual `aws-sdk` our mock; 8 | */ 9 | import test from 'ava'; 10 | import * as proxyquire from 'proxyquire'; 11 | import invokeLambdaHandler from './util/invokeLambdaHandler'; 12 | 13 | class MockSTS { 14 | getCallerIdentity () { 15 | return { 16 | promise () { 17 | return new Promise((resolve, reject) => { 18 | resolve({ 19 | Account: '123456789012', 20 | Arn: 'arn:aws:iam::123456789012:user/Alice', 21 | UserId: 'AKIAI44QH8DHBEXAMPLE' 22 | }); 23 | }); 24 | } 25 | }; 26 | } 27 | } 28 | 29 | const MockAWS = { 30 | STS: MockSTS 31 | }; 32 | 33 | const lambdaCallerIdentity = proxyquire('~/src/lambdas/aws-sts-caller-identity', { 34 | 'aws-sdk': MockAWS 35 | }); 36 | 37 | test('aws-sts-caller-identity lambda handler should return result', async (t) => { 38 | const context = {} as AWSLambda.Context; 39 | const result: any = await invokeLambdaHandler(lambdaCallerIdentity.handler, {}, context); 40 | t.deepEqual(result, { 41 | Account: '123456789012', 42 | Arn: 'arn:aws:iam::123456789012:user/Alice', 43 | UserId: 'AKIAI44QH8DHBEXAMPLE' 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/unit/lambda-func1.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { handler as func1Handler } from '~/src/lambdas/func1'; 3 | import invokeLambdaHandler from './util/invokeLambdaHandler'; 4 | 5 | test('func1 lambda handler should return proper name', async (t) => { 6 | const context = {} as AWSLambda.Context; 7 | const result: any = await invokeLambdaHandler(func1Handler, {}, context); 8 | t.is(result.name, 'func1'); 9 | }); 10 | -------------------------------------------------------------------------------- /test/unit/lambda-func2.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { handler as func2Handler } from '~/src/lambdas/func2'; 3 | import invokeLambdaHandler from './util/invokeLambdaHandler'; 4 | 5 | test('func2 lambda handler should return correct name and message if command ends with "world"', async (t) => { 6 | const context = {} as AWSLambda.Context; 7 | const input = { command: 'hello world' }; 8 | const result = await invokeLambdaHandler(func2Handler, input, context); 9 | t.deepEqual(result, { 10 | name: 'func2', 11 | message: 'hello to world' 12 | }); 13 | }); 14 | 15 | test('func2 lambda handler should return correct name and message if command does not end with "world"', async (t) => { 16 | const context = {} as AWSLambda.Context; 17 | const event = { command: 'hello blah' }; 18 | const result = await invokeLambdaHandler(func2Handler, event, context); 19 | t.deepEqual(result, { 20 | name: 'func2', 21 | message: 'hello to something else' 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/util.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as util from '~/src/util'; 3 | 4 | test('addAsync should add two numbers asynchronously', async (t) => { 5 | const answer = await util.addAsync(1, 2); 6 | t.is(answer, 3); 7 | }); 8 | -------------------------------------------------------------------------------- /test/unit/util/invokeLambdaHandler.ts: -------------------------------------------------------------------------------- 1 | export default async function invokeLambdaHandler (handler: any, event: any, context: AWSLambda.Context) { 2 | return new Promise((resolve, reject) => { 3 | handler(event, context, (err: any, result: any) => { 4 | if (err) { 5 | return reject(err); 6 | } 7 | resolve(result); 8 | }); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /tools/bin/zip-lambdas: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | ( 4 | lambda_names=() 5 | 6 | pushd ./src/lambdas > /dev/null 7 | LAMBDA_DIRS=`find . -type d` 8 | popd > /dev/null 9 | 10 | DIST_DIR='work/dist' 11 | # Read the directory `src/lambda/*` directory 12 | # to find names of all of the lambda functions. 13 | for LAMBDA_DIR in ${LAMBDA_DIRS}; do 14 | if [ "$LAMBDA_DIR" != '.' ]; then 15 | LAMBDA_NAME="$( basename ${LAMBDA_DIR} )" 16 | pushd "${DIST_DIR}/${LAMBDA_NAME}" > /dev/null 17 | LAMBDA_ZIP_FILE="../${LAMBDA_NAME}.zip" 18 | rm -f "${LAMBDA_ZIP_FILE}" 19 | zip -r "${LAMBDA_ZIP_FILE}" . 20 | popd > /dev/null 21 | 22 | echo "Built ${DIST_DIR}/${LAMBDA_NAME}.zip" 23 | fi 24 | done 25 | ) 26 | -------------------------------------------------------------------------------- /tsconfig-src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./work/dist", 5 | "module": "ES6" 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./work/dist-test", 5 | "module": "commonjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "baseUrl": ".", 5 | "outDir": "./work/dist", 6 | "noUnusedLocals": true, 7 | "allowSyntheticDefaultImports": false, 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "alwaysStrict": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "allowJs": true, 14 | "moduleResolution": "node", 15 | "target": "ESNext", 16 | "pretty": true, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "paths": { 20 | "~/*": ["./*"] 21 | } 22 | }, 23 | "include": [ 24 | "src/**/*", 25 | "test/**/*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-semistandard" 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable security/detect-non-literal-fs-filename */ 2 | /* eslint-disable security/detect-object-injection */ 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const nodeBuiltins = require('builtin-modules'); 6 | 7 | const lambdaDir = 'src/lambdas'; 8 | const lambdaNames = fs.readdirSync(path.join(__dirname, lambdaDir)); 9 | 10 | const DIST_DIR = path.join(__dirname, 'work/dist'); 11 | 12 | const entry = lambdaNames 13 | .reduce((entryMap, lambdaName) => { 14 | entryMap[lambdaName] = [ 15 | 'source-map-support/register', 16 | path.join(DIST_DIR, lambdaDir, `${lambdaName}/index.js`) 17 | ]; 18 | return entryMap; 19 | }, {}); 20 | 21 | const externals = ['aws-sdk'] 22 | .concat(nodeBuiltins) 23 | .reduce((externalsMap, moduleName) => { 24 | externalsMap[moduleName] = moduleName; 25 | return externalsMap; 26 | }, {}); 27 | 28 | module.exports = { 29 | entry, 30 | externals, 31 | 32 | output: { 33 | path: path.join(__dirname, 'work/dist'), 34 | libraryTarget: 'commonjs', 35 | filename: '[name]/index.js' 36 | }, 37 | 38 | target: 'node', 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.js$/, 44 | exclude: [], 45 | use: 'babel-loader' 46 | } 47 | ] 48 | }, 49 | 50 | resolve: { 51 | alias: { 52 | '~': DIST_DIR 53 | } 54 | }, 55 | 56 | devtool: 'source-map' 57 | }; 58 | --------------------------------------------------------------------------------