├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── workflow.yml ├── .gitignore ├── .jsbeautifyrc ├── .jshintrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── node-lambda ├── index.js ├── index_mjs.mjs ├── lib ├── .env.example ├── aws.js ├── cloudwatch_logs.js ├── context.json.example ├── deploy.env.example ├── event.json.example ├── event_sources.json.example ├── main.js ├── s3_deploy.js ├── s3_events.js └── schedule_events.js ├── node-lambda.png ├── package-lock.json ├── package.json ├── tea.yaml ├── test ├── cloudwatch_logs.js ├── handler │ └── index.js ├── main.js ├── node-lambda.js ├── post_install.sh ├── s3_deploy.js ├── s3_events.js ├── schedule_events.js └── testPj │ ├── index.js │ ├── package-lock.json │ └── package.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | root = true 5 | 6 | [*] 7 | 8 | # Change these settings to your own preference 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # We recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | schedule: 10 | - cron: '20 21 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'javascript' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | # If you wish to specify custom queries, you can do so here or in a config file. 36 | # By default, queries listed here will override any specified in a config file. 37 | # Prefix the list here with "+" to use these queries and those in the config file. 38 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 39 | 40 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 41 | # If this step fails, then you should remove it and run the build manually (see below) 42 | - name: Autobuild 43 | uses: github/codeql-action/autobuild@v3 44 | 45 | # ℹ️ Command-line programs to run using the OS shell. 46 | # 📚 https://git.io/JvXDl 47 | 48 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 49 | # and modify them (or add more) to build your code if your project 50 | # uses a compiled language 51 | 52 | #- run: | 53 | # make bootstrap 54 | # make release 55 | 56 | - name: Perform CodeQL Analysis 57 | uses: github/codeql-action/analyze@v3 58 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ubuntu-test: 7 | name: Test on node ${{ matrix.node-version }} and Ubuntu 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: [20.x, 22.x] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | cache: 'npm' 19 | - run: node -v 20 | - run: npm -v 21 | - run: npm ci 22 | - run: npm test 23 | 24 | mac-and-windows-test: 25 | name: Test on node latest and ${{ matrix.os }} 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: [windows-latest, macos-latest] 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: 22.x 36 | cache: 'npm' 37 | - run: node -v 38 | - run: npm -v 39 | - run: npm ci 40 | - run: npm test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############ 2 | # Node # 3 | ############ 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Commenting this out is preferred by some people, see 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | !.env.example 34 | .env 35 | event.json 36 | context.json 37 | deploy.env 38 | event_sources.json 39 | npm-debug.log* 40 | .lambda 41 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "break_chained_methods": false, 3 | "e4x": false, 4 | "eval_code": false, 5 | "indent_char": " ", 6 | "indent_level": 0, 7 | "indent_size": 2, 8 | "indent_with_tabs": false, 9 | "jslint_happy": true, 10 | "keep_array_indentation": false, 11 | "keep_function_indentation": false, 12 | "max_preserve_newlines": 2, 13 | "preserve_newlines": true, 14 | "space_before_conditional": true, 15 | "space_in_paren": false, 16 | "unescape_strings": false, 17 | "wrap_line_length": 120, 18 | "space_after_anon_function":true 19 | } 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "maxerr" : 50, // {int} Maximum error before stopping 4 | "esversion" : 6, 5 | 6 | // Enforcing 7 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 8 | "camelcase" : false, // false: Identifiers do not need to be in camelCase. We would like to enforce this except for when interacting with our api objects which use snakeCase properties. 9 | "curly" : false, // false: Do not require {} for every new block or scope 10 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 11 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 12 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 13 | "indent" : 2, // {int} Number of spaces to use for indentation 14 | "latedef" : false, // true: Require variables/functions to be defined before being used 15 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 16 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 17 | "noempty" : true, // true: Prohibit use of empty blocks 18 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 19 | "plusplus" : false, // true: Prohibit use of `++` & `--` 20 | "quotmark" : false, // Quotation mark consistency: 21 | // false : do nothing (default) 22 | // true : ensure whatever is used is consistent 23 | // "single" : require single quotes 24 | // "double" : require double quotes 25 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 26 | "unused" : true, // true: Require all defined variables be used 27 | "strict" : false, // false: Do not require all functions to be run in ES5 Strict Mode 28 | "trailing" : true, // true: Prohibit trailing whitespaces 29 | "maxparams" : false, // {int} Max number of formal params allowed per function 30 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 31 | "maxstatements" : false, // {int} Max number statements per function 32 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 33 | "maxlen" : 120, // {int} Max number of characters per line 34 | 35 | // Relaxing 36 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 37 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 38 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 39 | "eqnull" : false, // true: Tolerate use of `== null` 40 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 41 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 42 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 43 | // (ex: `for each`, multiple try/catch, function expression…) 44 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 45 | "expr" : true, // true: Tolerate `ExpressionStatement` as Programs 46 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 47 | "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') 48 | "iterator" : false, // true: Tolerate using the `__iterator__` property 49 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 50 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 51 | "laxcomma" : false, // true: Tolerate comma-first style coding 52 | "loopfunc" : false, // true: Tolerate functions being defined in loops 53 | "multistr" : false, // true: Tolerate multi-line strings 54 | "proto" : false, // true: Tolerate using the `__proto__` property 55 | "scripturl" : false, // true: Tolerate script-targeted URLs 56 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 57 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 58 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 59 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 60 | "validthis" : false, // true: Tolerate using this in a non-constructor function 61 | 62 | // Environments 63 | "browser" : false, // Web Browser (window, document, etc) 64 | "couch" : false, // CouchDB 65 | "devel" : false, // Development/debugging (alert, confirm, etc) 66 | "dojo" : false, // Dojo Toolkit 67 | "jquery" : false, // jQuery 68 | "mootools" : false, // MooTools 69 | "node" : true, // Node.js 70 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 71 | "prototypejs" : false, // Prototype and Scriptaculous 72 | "rhino" : false, // Rhino 73 | "worker" : false, // Web Workers 74 | "wsh" : false, // Windows Scripting Host 75 | "yui" : false, // Yahoo User Interface 76 | 77 | "ignore": [{"dir":"./node_modules"}], 78 | 79 | 80 | // Custom globals, from http://docs.meteor.com, in the order they appear there 81 | "globals" : { 82 | // Jasmine 83 | "expect": false, 84 | "beforeEach": false, 85 | "afterEach":false, 86 | "beforeAll": false, 87 | "afterAll": false, 88 | "it": false, 89 | "describe": false, 90 | "spyOn": false 91 | } 92 | 93 | 94 | } 95 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.8.0] - 2016-04-18 8 | ### Added 9 | - CHANGELOG to ["make it easier for users and contributors to see precisely what notable changes have been made between each release"](http://keepachangelog.com/). Linked to from README 10 | - LICENSE to be more explicit about what was defined in `package.json`. Linked to from README 11 | - It is OK to not set default value for AWS Credentials so AWS can use Roles and internally set AWS credentials 12 | - Added `context.json` so it can easily be overwritten 13 | - Allow using a custom (and passed through) `event.json` file 14 | - Added `package` command for easy zip creation and inspection 15 | - Added `VpcConfig` support, see [this PR](https://github.com/motdotla/node-lambda/pull/64) for more information 16 | - Updated the AWS API version used to `2015-03-31` 17 | - Make sure we throw errors on unrecoverable failures so other programs can listen on that 18 | - Added support for nodejs4.3 runtime ([introducted to AWS](https://aws.amazon.com/blogs/compute/node-js-4-3-2-runtime-now-available-on-lambda/) Apr 7 2016) 19 | - Added support for `post install scripts`, this `post_install.sh` file will be triggered after `npm install --production` in case you want to run any code on your application before zipping 20 | - Added `-x` / `--excludeGlobs` to allow custom file exclusion 21 | - Excluding `*.swp`, `deploy.env` by default now 22 | 23 | ## [0.8.1] - 2016-04-22 24 | ### Bugfixes 25 | - Resolved a problem with excludes not being set [#91](https://github.com/motdotla/node-lambda/pull/91) 26 | - Resolved a problem with the package command and a custom config file [#90](https://github.com/motdotla/node-lambda/pull/90) 27 | - Allow `use strict` [#86](https://github.com/motdotla/node-lambda/pull/86) 28 | - Updated the `env.example` file to set the default (and by AWS recommended) runtime to `nodejs4.3` [#84](https://github.com/motdotla/node-lambda/pull/84) 29 | 30 | ## [0.8.2] - 2016-05-12 31 | ### Bugfixes 32 | - Verify env vars before creating sample files [#99](https://github.com/motdotla/node-lambda/pull/99) 33 | - Fix `AWS_PUBLIS` typo [#102](https://github.com/motdotla/node-lambda/pull/102) 34 | ### Added 35 | - Allow checking on `process.env.environment` to context switch [#95](https://github.com/motdotla/node-lambda/pull/95) 36 | 37 | ## [0.8.3] - 2016-05-12 38 | ### Bugfixes 39 | - Added `EXCLUDE_GLOBS` to `package`, so your local ZIPs are the same as the ZIPs uploaded to AWS Lambda [#104](https://github.com/motdotla/node-lambda/pull/104) 40 | 41 | ## [0.8.4] - 2016-05-20 42 | ### Bugfixes 43 | - Added extra quotes around the parsed environment [#106](https://github.com/motdotla/node-lambda/pull/106) 44 | 45 | ## [0.8.5] - 2016-05-27 46 | ### Adjustment 47 | - Extremely verbose NPM installs could crash node-lambda's buffer [#108](https://github.com/motdotla/node-lambda/pull/108) 48 | 49 | ## [0.8.6] - 2016-06-28 50 | ### Feature 51 | - Added `prebuiltDirectory` flag for users that want to use an already generated directory [#116](https://github.com/motdotla/node-lambda/pull/116) 52 | 53 | ### Bugfixes 54 | - README was lying about how to use `excludeGlobs` [#111](https://github.com/motdotla/node-lambda/pull/111) 55 | 56 | ## [0.8.7] - 2016-08-16 57 | ### Features 58 | - Added `-L` to rsync to allow copying of symlinks [#126](https://github.com/motdotla/node-lambda/pull/126) 59 | - Added travisci support for node 6 [#129](https://github.com/motdotla/node-lambda/pull/129) 60 | - Support to use package.json description for AWS description [#133](https://github.com/motdotla/node-lambda/pull/133) 61 | - Inject environment variables via config file for the `run` command [#136](https://github.com/motdotla/node-lambda/pull/136) 62 | 63 | ### Bugfixes 64 | - rsync should not exclude node_modules when using --prebuiltDirectory. [#122](https://github.com/motdotla/node-lambda/pull/122) 65 | - Set environment variables _before_ requiring module [#137](https://github.com/motdotla/node-lambda/pull/137) 66 | - Typo fix publish when updating existing function [#138](https://github.com/motdotla/node-lambda/pull/138) 67 | 68 | ## [0.8.8] - 2016-09-02 69 | ### Features 70 | - Support AWS_PROFILE and de-duped a few CLI options [#144](https://github.com/motdotla/node-lambda/pull/144) 71 | - `wrench` was deprecated and has been replaced by `fs-extra` [#146](https://github.com/motdotla/node-lambda/pull/146) 72 | 73 | ### Bugs 74 | - Displaying `node-lambda -h` returned an error [#127](https://github.com/motdotla/node-lambda/issues/127) 75 | - NPM overwrites `$TMPDIR` [#134](https://github.com/motdotla/node-lambda/issues/134) 76 | 77 | ## [0.8.9] - 2016-09-06 78 | ### Bugs 79 | - The above mentioned fix for issue [#127](https://github.com/motdotla/node-lambda/issues/127) exposed a commander bug, reverted the change 80 | - Do not exclude package.json, even when specified in excludeGlobs [#141](https://github.com/motdotla/node-lambda/pull/141) 81 | 82 | ## [0.8.10] - 2016-09-20 83 | ### Features 84 | - We are now passing the environment string to the post install script [#154](https://github.com/motdotla/node-lambda/pull/154) 85 | 86 | ## [0.8.11] - 2016-10-28 87 | ### Bugfixes 88 | - Restore lambda version functionality [#156](https://github.com/motdotla/node-lambda/issues/156) 89 | - Namespaced packages fail to deploy [#157](https://github.com/motdotla/node-lambda/issues/157) 90 | 91 | ## [0.8.12] - 2017-02-10 92 | ### Bugfixes 93 | - Using path.join instead of hardcoded slashes [#173](https://github.com/motdotla/node-lambda/pull/173) 94 | - Drop node-uuid from package.json [#174](https://github.com/motdotla/node-lambda/pull/174) 95 | - Enforce max for timeout and update README docs [#180](https://github.com/motdotla/node-lambda/pull/180) 96 | - Fill default VpcConfig to prevent errors [#183](https://github.com/motdotla/node-lambda/pull/183) 97 | 98 | ### Features 99 | - Added getRemainingTimeInMillis() to the context when running locally. [#179](https://github.com/motdotla/node-lambda/pull/179) 100 | - Adding support for lambda environment variables [#181](https://github.com/motdotla/node-lambda/pull/181) 101 | 102 | ## [0.8.13] - 2017-02-12 103 | ### Bugfixes 104 | - Fixed wrong runtime call [#188](https://github.com/motdotla/node-lambda/pull/188) 105 | - Docker support [#186](https://github.com/motdotla/node-lambda/pull/186) 106 | - Make default excludes apply to root only [#185](https://github.com/motdotla/node-lambda/pull/185) 107 | 108 | 109 | ## [0.8.14] - 2017-03-27 110 | ### Features 111 | - Event source mapping support [#189](https://github.com/motdotla/node-lambda/pull/189) 112 | - Fix version of Node.js supported by AWS Lambda [#197](https://github.com/motdotla/node-lambda/pull/197) 113 | - How about it if you have the option to specify the zip file? [#199](https://github.com/motdotla/node-lambda/pull/199) 114 | - Add 'Runtime' to the params of lambda.updateFunctionConfiguration [#200](https://github.com/motdotla/node-lambda/pull/200) 115 | 116 | ### Bugfixes 117 | - Fix unit test failure at travis [#198](https://github.com/motdotla/node-lambda/pull/198) 118 | 119 | ## [0.8.15] - 2017-03-28 120 | ### Features 121 | - Added DeadLetterConfig parameter [#206](https://github.com/motdotla/node-lambda/pull/206) 122 | 123 | ### Bugfixes 124 | - Fix default value of EVENT_SOURCE_FILE set '' [#205](https://github.com/motdotla/node-lambda/pull/205) 125 | - Removed event_sources.json [#204](https://github.com/motdotla/node-lambda/pull/204) 126 | - Add -S, --eventSourceFile option. [#203](https://github.com/motdotla/node-lambda/pull/203) 127 | 128 | ## [0.9.0] - 2017-04-13 129 | ### Features 130 | - Add tests for `_readArchive` [#213](https://github.com/motdotla/node-lambda/pull/213) 131 | - Add tests for event_sources.json [#214](https://github.com/motdotla/node-lambda/pull/214) 132 | - Add tests for DeadLetterConfig [#215](https://github.com/motdotla/node-lambda/pull/215) 133 | - Add `_readArchive` tests called in `_archive` [#216](https://github.com/motdotla/node-lambda/pull/216) 134 | - modify badge from png to svg [#227](https://github.com/motdotla/node-lambda/pull/227) 135 | - ScheduleEvents [#228](https://github.com/motdotla/node-lambda/pull/228) 136 | 137 | ### Bugfixes 138 | - Bugfix InvalidParameterValueException is given when createFunction [#209](https://github.com/motdotla/node-lambda/pull/209) 139 | - Clean unnecessary `else` [#217](https://github.com/motdotla/node-lambda/pull/217) 140 | - Refactor `_eventSourceList` [#218](https://github.com/motdotla/node-lambda/pull/218) 141 | - Clean deploy function [#220](https://github.com/motdotla/node-lambda/pull/220) 142 | - Fix default value of params.Environment.Variables is null [#221](https://github.com/motdotla/node-lambda/pull/221) 143 | - Fix to use authenticated `aws` object in main.js [#225](https://github.com/motdotla/node-lambda/pull/225) 144 | - Changed the format of `event_sources.json` [#226](https://github.com/motdotla/node-lambda/pull/226) 145 | 146 | ## [0.10.0] - 2017-05-10 147 | ### Features 148 | - Fix use fs object [#236](https://github.com/motdotla/node-lambda/pull/236) 149 | - Upgrade lodash [#237](https://github.com/motdotla/node-lambda/pull/237) 150 | - Add file copy function without rsync command [#238](https://github.com/motdotla/node-lambda/pull/238) 151 | - Add node.js 7 to `travis.yml` [#239](https://github.com/motdotla/node-lambda/pull/239) 152 | - Set http timeout to 30 mins. [#240](https://github.com/motdotla/node-lambda/pull/240) 153 | - Supported `TracingConfig` [#243](https://github.com/motdotla/node-lambda/pull/243) 154 | - Fix to using `path` object [#249](https://github.com/motdotla/node-lambda/pull/249) 155 | - Allow use of docker container for npm install [#251](https://github.com/motdotla/node-lambda/pull/251) 156 | - Bugfix `_filecopy` exclude [#253](https://github.com/motdotla/node-lambda/pull/253) 157 | - Fix to replace `_rsync` with `_fileCopy` [#254](https://github.com/motdotla/node-lambda/pull/254) 158 | - Custom ScheduleEvent rule description [#257](https://github.com/motdotla/node-lambda/pull/257) 159 | - Add test `functionName` pattern [#263](https://github.com/motdotla/node-lambda/pull/263) 160 | - Added `- cwd` option to `npm install` command [#265](https://github.com/motdotla/node-lambda/pull/265) 161 | 162 | ### Bugfixes 163 | - Add an overview of `event_sources.json` [#230](https://github.com/motdotla/node-lambda/pull/230) 164 | - Cleanup of `main.js run` [#231](https://github.com/motdotla/node-lambda/pull/231) 165 | - Fix results outputs [#233](https://github.com/motdotla/node-lambda/pull/233) 166 | - Bugfix for backward compatible objects [#234](https://github.com/motdotla/node-lambda/pull/234) 167 | - Fix after process of tests [#235](https://github.com/motdotla/node-lambda/pull/235) 168 | - Fix to be the same specification as `--exclude` of rsync command (about function which is an alternative to rsync command) [#244](https://github.com/motdotla/node-lambda/pull/244) 169 | - Fix to avoid `commander` bug [#247](https://github.com/motdotla/node-lambda/pull/247) 170 | - Fix `fs.exists` deprecated [#250](https://github.com/motdotla/node-lambda/pull/250) 171 | - Fix using `assert.include` [#252](https://github.com/motdotla/node-lambda/pull/252) 172 | - Fix not doing anything if `event_sources.json` is not specified [#256](https://github.com/motdotla/node-lambda/pull/256) 173 | - Fix using `path` [#258](https://github.com/motdotla/node-lambda/pull/258) 174 | - Fix tests for windows [#259](https://github.com/motdotla/node-lambda/pull/259) 175 | - Add Command Prompt to README [#266](https://github.com/motdotla/node-lambda/pull/266) 176 | - Fix indexjs current style [#268](https://github.com/motdotla/node-lambda/pull/268) 177 | - Fixed typo - Labmda => Lambda [#269](https://github.com/motdotla/node-lambda/pull/269) 178 | - Fix not to create `.env` sample file with `_buildAndArchive` [#270](https://github.com/motdotla/node-lambda/pull/270) 179 | 180 | ## [0.11.0] - 2017-06-16 181 | ### Features 182 | - Fix to include only `package.json` in the source directory [#274](https://github.com/motdotla/node-lambda/pull/274) 183 | - Fix os: deprecate 'tmpDir()' in favour of 'tmpdir()' https://github.c… [#275](https://github.com/motdotla/node-lambda/pull/275) 184 | - Upgraded `aws-sdk`[#277](https://github.com/motdotla/node-lambda/pull/277) 185 | - Unified in Camel Case & Remove unnecessary arguments [#278](https://github.com/motdotla/node-lambda/pull/278) 186 | - Remove function `_nativeZip` [#279](https://github.com/motdotla/node-lambda/pull/279) 187 | - Add known issue for duplicate trigger to ScheduleEvents section [#280](https://github.com/motdotla/node-lambda/pull/280) 188 | - Feature simple callbackWaitsForEmptyEventLoop support [#284](https://github.com/motdotla/node-lambda/pull/284) 189 | - Fix to use test handler by stopping replace processing of existing index.js [#285](https://github.com/motdotla/node-lambda/pull/285) 190 | - Fix to use '===' instead of '==' (Including similar modifications) [#287](https://github.com/motdotla/node-lambda/pull/287) 191 | - Replaced `rimraf` with `fs.remove` [#291](https://github.com/motdotla/node-lambda/pull/291) 192 | - Refactored: JavaScript Standard Style [#292](https://github.com/motdotla/node-lambda/pull/292) 193 | - Refactored and add test [#294](https://github.com/motdotla/node-lambda/pull/294) 194 | - Refactored, improved error handling [#295](https://github.com/motdotla/node-lambda/pull/295) 195 | - Remove semicolon (Automatic update with `standard --fix`) [#298](https://github.com/motdotla/node-lambda/pull/298) 196 | - Adopted "JavaScript Standard Style" as coding style [#299](https://github.com/motdotla/node-lambda/pull/299) 197 | - Replace async.js in `_updateScheduleEvents` with Promise [#302](https://github.com/motdotla/node-lambda/pull/302) 198 | - Modify from `exec` to `execFile` with `_npmInstall` [#303](https://github.com/motdotla/node-lambda/pull/303) 199 | - Automated NPM deploys (on tags) [#304](https://github.com/motdotla/node-lambda/pull/304) 200 | - Add package-lock.json [#305](https://github.com/motdotla/node-lambda/pull/305) 201 | - Added `_updateEventSources` test and refactoring [#308](https://github.com/motdotla/node-lambda/pull/308) 202 | - Added test of function to upload Zip to Lambda [#309](https://github.com/motdotla/node-lambda/pull/309) 203 | - Fix timing to check Runtime [#310](https://github.com/motdotla/node-lambda/pull/310) 204 | - Feature event accept array [#311](https://github.com/motdotla/node-lambda/pull/311) 205 | - Modify to use isArray [#312](https://github.com/motdotla/node-lambda/pull/312) 206 | - Modify execution of multiple events to synchronous processing [#313](https://github.com/motdotla/node-lambda/pull/313) 207 | - Fix to make `bin/node-lambda` conform to JavaScript Standard Style [#315](https://github.com/motdotla/node-lambda/pull/315) 208 | - Replace asyncjs of `_updateEventSources` with Promise [#316](https://github.com/motdotla/node-lambda/pull/316) 209 | - Replace async.js of deploy with Promise [#319](https://github.com/motdotla/node-lambda/pull/319) 210 | - Modified the function used in `deploy` to return Promise [#320](https://github.com/motdotla/node-lambda/pull/320) 211 | - Modify main process of deploy to another function [#323](https://github.com/motdotla/node-lambda/pull/323) 212 | - Fix to use Proxy [#324](https://github.com/motdotla/node-lambda/pull/324) 213 | 214 | ### Bugfixes 215 | - Remove the cleaning process after the test [#281](https://github.com/motdotla/node-lambda/pull/281) 216 | - Fix run handler callback [#282](https://github.com/motdotla/node-lambda/pull/282) 217 | - Remove 'os' [#286](https://github.com/motdotla/node-lambda/pull/286) 218 | - Fix not specifying file name with test in package.json [#289](https://github.com/motdotla/node-lambda/pull/289) 219 | - Update phase as it is necessary to separate release phase (.travis.yml) [#306](https://github.com/motdotla/node-lambda/pull/306) 220 | - Refactoring and unnecessary package removal [#307](https://github.com/motdotla/node-lambda/pull/307) 221 | - Modify `var` of global variable to `const` [#317](https://github.com/motdotla/node-lambda/pull/317) 222 | - Remove Hoek [#318](https://github.com/motdotla/node-lambda/pull/318) 223 | 224 | ## [0.11.1] - 2017-07-04 225 | ### Features 226 | - Improve deploy process with Promise [#327](https://github.com/motdotla/node-lambda/pull/327) 227 | - Refactoring `_cleanDirectory` [#330](https://github.com/motdotla/node-lambda/pull/330) 228 | - Refactoring `_npmInstall` [#331](https://github.com/motdotla/node-lambda/pull/331) 229 | - Replace callback with Promise [#332](https://github.com/motdotla/node-lambda/pull/332) 230 | - Upgrade commander.js [#334](https://github.com/motdotla/node-lambda/pull/332) 231 | - Refactoring `_fileCopy` [#336](https://github.com/motdotla/node-lambda/pull/336) 232 | - Add simple context method [#337](https://github.com/motdotla/node-lambda/pull/337) 233 | - Refactoring `_archive` [#338](https://github.com/motdotla/node-lambda/pull/338) 234 | - Refactoring `_listEventSourceMappings` [#339](https://github.com/motdotla/node-lambda/pull/339) 235 | - Replace `var` with `const` [#341](https://github.com/motdotla/node-lambda/pull/341) 236 | - Replace with arrow function [#342](https://github.com/motdotla/node-lambda/pull/342) 237 | 238 | ### Bugfixes 239 | - Modify file used for test [#326](https://github.com/motdotla/node-lambda/pull/326) 240 | - Update package-lock.json [#328](https://github.com/motdotla/node-lambda/pull/328) 241 | - Remove `_rsync` [#329](https://github.com/motdotla/node-lambda/pull/329) 242 | - Bugfixed that mode of file changes when zip is created [#335](https://github.com/motdotla/node-lambda/pull/335) 243 | 244 | ## [0.11.2] - 2017-07-05 245 | ### Features 246 | - Fix to deprecated the `configFile` option in the `pacakage` command [#344](https://github.com/motdotla/node-lambda/pull/344) 247 | 248 | ### Bugfixes 249 | - Fix to set boolean in params.Publish [#346](https://github.com/motdotla/node-lambda/pull/346) 250 | 251 | ## [0.11.3] - 2017-07-07 252 | ### Features 253 | - Fix symlink at zip [#348](https://github.com/motdotla/node-lambda/pull/348) 254 | 255 | ## [0.11.4] - 2017-09-22 256 | ### Features 257 | - Remove configFile Option of package command in README [#350](https://github.com/motdotla/node-lambda/pull/350) 258 | - Remove configFile option in package command [#351](https://github.com/motdotla/node-lambda/pull/351) 259 | - Uprade chai [#352](https://github.com/motdotla/node-lambda/pull/352) 260 | - Add the ability to set KMSKeyArn to a Lambda function [#356](https://github.com/motdotla/node-lambda/pull/356) 261 | - Add appveyor.yml[#357](https://github.com/motdotla/node-lambda/pull/357) 262 | - Add function for setting CloudWatchLogs RetentionPolicy [#359](https://github.com/motdotla/node-lambda/pull/359) 263 | - Switch ScheduleEvents to class syntax [#360](https://github.com/motdotla/node-lambda/pull/360) 264 | - Add `_setLogsRetentionPolicy` to `lib/main.js` [#361](https://github.com/motdotla/node-lambda/pull/361) 265 | - Change `lib/main.js` to class syntax [#362](https://github.com/motdotla/node-lambda/pull/362) 266 | - Use stable node, at npm release. [#370](https://github.com/motdotla/node-lambda/pull/370) 267 | - Add option to disable run multiple [#372](https://github.com/motdotla/node-lambda/pull/372) 268 | 269 | ### Bugfixes 270 | - Update repository url [#358](https://github.com/motdotla/node-lambda/pull/358) 271 | - Fix deploy command exit code [#366](https://github.com/motdotla/node-lambda/pull/366) 272 | - Add invalidation of log output to make the test result easier to read [#367](https://github.com/motdotla/node-lambda/pull/367) 273 | - Fix commandline version option [#368](https://github.com/motdotla/node-lambda/pull/368) 274 | - Change: Ensure dotenv.load called before AWS load [#369](https://github.com/motdotla/node-lambda/pull/369) 275 | - Update README with latest output for 'node-lambda run -h' [#373](https://github.com/motdotla/node-lambda/pull/373) 276 | - Update Usage of README [#374](https://github.com/motdotla/node-lambda/pull/374) 277 | 278 | ## [0.11.5] - 2017-12-11 279 | ### Features 280 | - Move node-zip to devDependencies [#378](https://github.com/motdotla/node-lambda/pull/378) 281 | - Added the ability to set constants when scheduling a Lambda function Cloudwatch event [#380](https://github.com/motdotla/node-lambda/pull/380) 282 | - Update CI's Node.js to LTS and latest version [#386](https://github.com/motdotla/node-lambda/pull/386) 283 | - Update packages [#392](https://github.com/motdotla/node-lambda/pull/392) 284 | - Added class to set S3 events [#393](https://github.com/motdotla/node-lambda/pull/393) 285 | - Add updateS3Events to main [#394](https://github.com/motdotla/node-lambda/pull/394) 286 | - Refactoring lib/schedule_events.js [#395](https://github.com/motdotla/node-lambda/pull/395) 287 | 288 | ### Bugfixes 289 | - Set docker run working directory so npm install works [#381](https://github.com/motdotla/node-lambda/pull/381) 290 | - Change short option of `--tracingConfig` to `-c` [#385](https://github.com/motdotla/node-lambda/pull/385) 291 | - Fix to use Proxy when run locally [#389](https://github.com/motdotla/node-lambda/pull/389) 292 | 293 | ## [0.11.6] - 2018-01-07 294 | ### Features 295 | - Refactoring lib/main.js [#398](https://github.com/motdotla/node-lambda/pull/398) 296 | - Remove unnecessary `return this` for constructor [#399](https://github.com/motdotla/node-lambda/pull/399) 297 | - Remove unnecessary try-cache [#401](https://github.com/motdotla/node-lambda/pull/401) 298 | - Add event_sources.json to setup message [#402](https://github.com/motdotla/node-lambda/pull/402) 299 | - Modify using template literals [#403](https://github.com/motdotla/node-lambda/pull/403) 300 | - Remove unnecessary promise chain [#404](https://github.com/motdotla/node-lambda/pull/404) 301 | - Local/Cloud flag [#405](https://github.com/motdotla/node-lambda/pull/405) 302 | 303 | ## [0.11.7] - 2018-04-12 304 | ### Features 305 | - AWS X-Ray SDK Support [#414](https://github.com/motdotla/node-lambda/pull/414) 306 | - Upgrade `standard` [#416](https://github.com/motdotla/node-lambda/pull/416) 307 | - Added support for using custom endpoints like localstack [#417](https://github.com/motdotla/node-lambda/pull/417) 308 | - NodeJS 8.10 runtime now available [#419](https://github.com/motdotla/node-lambda/pull/419) 309 | 310 | ### Bugfixes 311 | - remove env var value from commanderjs flag definition [#409](https://github.com/motdotla/node-lambda/pull/409) 312 | 313 | ## [0.12.0] - 2018-08-10 314 | ### Features 315 | - Implemente to specify bucket name of S3 [#458](https://github.com/motdotla/node-lambda/pull/458) 316 | - Implement deployment using S3 (Create a bucket for each region.) [#455](https://github.com/motdotla/node-lambda/pull/455) 317 | - Add class for uploading deploy package to S3 [#454](https://github.com/motdotla/node-lambda/pull/454) 318 | - Fix to throw an error except ResourceNotFoundException [#452](https://github.com/motdotla/node-lambda/pull/452) 319 | - Feature upload to s3 and deploy from bucket [#446](https://github.com/motdotla/node-lambda/pull/446) 320 | - npm update [#445](https://github.com/motdotla/node-lambda/pull/445) 321 | - Upgrade dependent packages [#441](https://github.com/motdotla/node-lambda/pull/441) 322 | - Add simple test of `_deployToRegion()` and `deploy()` [#439](https://github.com/motdotla/node-lambda/pull/439) 323 | - Remove unnecessary package load in `test/main.js` [#438](https://github.com/motdotla/node-lambda/pull/438) 324 | - Add cache of `node modules` to CI setting [#436](https://github.com/motdotla/node-lambda/pull/436) 325 | - Modify `require` to `{ }` statement [#435](https://github.com/motdotla/node-lambda/pull/435) 326 | - Fix to use `includes` instead of `indexOf` [#433](https://github.com/motdotla/node-lambda/pull/433) 327 | - Remove test code for Node.js4 [#432](https://github.com/motdotla/node-lambda/pull/432) 328 | - Upgrade `fs-extra` [#431](https://github.com/motdotla/node-lambda/pull/431) 329 | - Stop supporting Node.js 4 [#430](https://github.com/motdotla/node-lambda/pull/430) 330 | - Fix using `klaw` instead of `fs.walk` [#424](https://github.com/motdotla/node-lambda/pull/424) 331 | - Add Node.js10 to CI setting [#428](https://github.com/motdotla/node-lambda/pull/428) 332 | 333 | ### Bugfixes 334 | - Fix StatementId [#451](https://github.com/motdotla/node-lambda/pull/451) 335 | - Bugfix of initialValue of recude in s3events [#447](https://github.com/motdotla/node-lambda/pull/447) 336 | - Added handling to catch and log error return from async lambda [#443](https://github.com/motdotla/node-lambda/pull/443) 337 | - Log result of an async handler method by resolving promise if a promise [#440](https://github.com/motdotla/node-lambda/pull/440) 338 | - Fix to display return value of handler [#427](https://github.com/motdotla/node-lambda/pull/427) 339 | - Fix to set array when same bucket [#423](https://github.com/motdotla/node-lambda/pull/423) 340 | 341 | ## [0.13.0] - 2018-11-15 342 | ### Features 343 | - Drop nodejs4.3 [#469](https://github.com/motdotla/node-lambda/pull/469) 344 | - Update maximum timeout value from 300 to 900 [#465](https://github.com/motdotla/node-lambda/pull/465) 345 | - Modify to follow the rules of the new 'standard' [#463](https://github.com/motdotla/node-lambda/pull/463) 346 | - Add 'osx' to CI os [#464](https://github.com/motdotla/node-lambda/pull/464) 347 | - Update CI test to LTS version [#462](https://github.com/motdotla/node-lambda/pull/462) 348 | - Upgrade `archiver` [#460](https://github.com/motdotla/node-lambda/pull/460) 349 | 350 | ### Bugfixes 351 | - Fix value of StartingPosition [#467](https://github.com/motdotla/node-lambda/pull/467) 352 | 353 | ## [0.14.0] - 2019-05-25 354 | ### Features 355 | - Support Node.js 10.x [#487](https://github.com/motdotla/node-lambda/pull/487) 356 | - Add Node.js 12 to CI setting [#486](https://github.com/motdotla/node-lambda/pull/486) 357 | - Add file to configure aws authentication settings [#482](https://github.com/motdotla/node-lambda/pull/482) 358 | - Add layers option to readme [#481](https://github.com/motdotla/node-lambda/pull/481) 359 | - Add option to specify Lambda Layers [#480](https://github.com/motdotla/node-lambda/pull/480) 360 | - Upgrade packages [#479](https://github.com/motdotla/node-lambda/pull/479) 361 | - Add dockerVolumes option in package in order to mount additional volumes [#473](https://github.com/motdotla/node-lambda/pull/473) 362 | - Add keepNodeModules option in package [#472](https://github.com/motdotla/node-lambda/pull/472) 363 | 364 | ### Bugfixes 365 | - Remove 'packageDirectory' option from 'deploy' [#484](https://github.com/motdotla/node-lambda/pull/484) 366 | - Update s3deploy bucket handling [#475](https://github.com/motdotla/node-lambda/pull/475) 367 | - Fix Docker volume mount from OSX - #461 [#471](https://github.com/motdotla/node-lambda/pull/471) 368 | 369 | ## [0.15.0] - 2019-12-11 370 | ### Features 371 | - adds tagging on new and updated functions [#508](https://github.com/motdotla/node-lambda/pull/508) 372 | - Add nodejs12.x to runtime [#510](https://github.com/motdotla/node-lambda/pull/510) 373 | - Don't audit packages when installing [#505](https://github.com/motdotla/node-lambda/pull/505) 374 | - Use `ci` instead of `install` when installing packages [#502](https://github.com/motdotla/node-lambda/pull/502) 375 | - Add reference to TypeScript example/template [#497](https://github.com/motdotla/node-lambda/pull/497) 376 | - Drop nodejs6.10 [#495](https://github.com/motdotla/node-lambda/pull/495) 377 | - Warn on providing unknown commands [#494](https://github.com/motdotla/node-lambda/pull/494) 378 | - Fix tests 379 | - Fix GitHub Actions workflow [#506](https://github.com/motdotla/node-lambda/pull/506) 380 | - Fix `npm ci` test. [#509](https://github.com/motdotla/node-lambda/pull/509) 381 | - Remove appveyor.yml [#504](https://github.com/motdotla/node-lambda/pull/504) 382 | - Modify unit tests [#501](https://github.com/motdotla/node-lambda/pull/501) 383 | - Fix GitHub Actions workflow [#500](https://github.com/motdotla/node-lambda/pull/500) 384 | - Add GitHub Actions workflow [#499](https://github.com/motdotla/node-lambda/pull/499) 385 | 386 | ## [0.16.0] - 2020-02-12 387 | ### Features 388 | - Remove osx from travis [#513](https://github.com/motdotla/node-lambda/pull/513) 389 | - Drop nodejs8.10 from runtime [#516](https://github.com/motdotla/node-lambda/pull/516) 390 | 391 | ## [0.17.0] - 2020-05-14 392 | ### Features 393 | - Implement a simple API Gateway event [#530](https://github.com/motdotla/node-lambda/pull/530) 394 | - [README] Add the 'apiGateway' option to the run command [#532](https://github.com/motdotla/node-lambda/pull/532) 395 | - Add Node.js 14 to CI settings [#524](https://github.com/motdotla/node-lambda/pull/524) 396 | - Drop old Node support [#523](https://github.com/motdotla/node-lambda/pull/523) 397 | - Bump acorn from 7.0.0 to 7.1.1 [#522](https://github.com/motdotla/node-lambda/pull/522) 398 | - Add Silent or quiet mode when deploying [#520](https://github.com/motdotla/node-lambda/pull/520) 399 | - [README update] Add silent option to deploy command [#521](https://github.com/motdotla/node-lambda/pull/521) 400 | - Update README (remove --endpoint of run subcommand, add --endpoint of deploy subcommand) [#514](https://github.com/motdotla/node-lambda/pull/514) 401 | 402 | ### Bugfixes 403 | - Upgrade "aws-xray-sdk-core" [#529](https://github.com/motdotla/node-lambda/pull/529) 404 | - Fix Lambda update failure [#526](https://github.com/motdotla/node-lambda/pull/526) 405 | - Fix typo [#527](https://github.com/motdotla/node-lambda/pull/527) 406 | 407 | ## [0.18.0] - 2021-02-19 408 | ### Features 409 | - feat: support nodejs14.x runtime [#553](https://github.com/motdotla/node-lambda/pull/553) 410 | - Upgrade Mocha to fix high sev vulnerability. [#551](https://github.com/motdotla/node-lambda/pull/551) 411 | - docs: add a note to the README about deploying container image [#549](https://github.com/motdotla/node-lambda/pull/549) 412 | - Support npm7 #[550](https://github.com/motdotla/node-lambda/pull/550) 413 | - feat: support for 'ImageUri' parameter [#548](https://github.com/motdotla/node-lambda/pull/548) 414 | - upgrade 'commander' to 7 [#547](https://github.com/motdotla/node-lambda/pull/547) 415 | - ci: add 'fail-fast: false' setting #[546](https://github.com/motdotla/node-lambda/pull/546) 416 | - use starsWith instead of indexOf [#545](https://github.com/motdotla/node-lambda/pull/545) 417 | - Upgrade 'standard' [#543](https://github.com/motdotla/node-lambda/pull/543) 418 | - Update S3_LOCATION_POSSIBLE_VALUES [#542](https://github.com/motdotla/node-lambda/pull/542) 419 | - Bump bl from 4.0.2 to 4.0.3 [#541](https://github.com/motdotla/node-lambda/pull/541) 420 | - Add description of vpc options [#540](https://github.com/motdotla/node-lambda/pull/540) 421 | - Upgrade packages [#538](https://github.com/motdotla/node-lambda/pull/538) 422 | - Bump lodash from 4.17.15 to 4.17.19 [#536](https://github.com/motdotla/node-lambda/pull/536) 423 | - Add build badge in README [#534](https://github.com/motdotla/node-lambda/pull/534) 424 | 425 | ## [0.19.0] - 2021-03-30 426 | ### Features 427 | - feat: support `--no-optional` option in npm install [#557](https://github.com/motdotla/node-lambda/pull/557) 428 | 429 | ## [0.19.1] - 2021-04-24 430 | ### Bugfixes 431 | - Fix errors caused by old proxy-agent [#564](https://github.com/motdotla/node-lambda/pull/564) 432 | 433 | ## [0.20.0] - 2021-09-25 434 | ### Features 435 | - updated proxy-agent to 5.0.0 [#574](https://github.com/motdotla/node-lambda/pull/574) 436 | - show suggestions after an error for an unknown command or option [#572](https://github.com/motdotla/node-lambda/pull/572) 437 | - feat: drop nodejs10x from lambda runtime [#571](https://github.com/motdotla/node-lambda/pull/571) 438 | 439 | ### Bugfixes 440 | - fix(_uploadExisting): fix function update errors [#575](https://github.com/motdotla/node-lambda/pull/575) 441 | - test: fix npm install test failing in some cases [#569](https://github.com/motdotla/node-lambda/pull/569) 442 | - Clean the tmp dir during `_archivePrebuilt` to match `_buildAndArchive` behavior [#518](https://github.com/motdotla/node-lambda/pull/518) 443 | 444 | ## [0.21.0] - 2021-11-10 445 | ### Features 446 | - feat: support for yarn [#581](https://github.com/motdotla/node-lambda/pull/581) 447 | 448 | ## [0.22.0] - 2022-02-17 449 | ### Features 450 | - Support for Architectures parameter [#591](https://github.com/motdotla/node-lambda/pull/591) 451 | ### Bugfixes 452 | - fix: skip installing the package, when there is no `package.json` [#589](https://github.com/motdotla/node-lambda/pull/589) 453 | 454 | ## [1.0.0] - 2022-05-19 455 | ### Features 456 | - feat: remove BUILD from the exclusion list [#607](https://github.com/motdotla/node-lambda/pull/607) 457 | - BREAKING CHANGES 458 | - add nodejs16.x to runtime [#605](https://github.com/motdotla/node-lambda/pull/605) 459 | 460 | ## [1.1.0] - 2022-10-26 461 | ### Features 462 | - feat: support esm [#613](https://github.com/motdotla/node-lambda/pull/613) 463 | 464 | ### Bugfixes 465 | - fix the bug in `excludeGlobs` in `_fileCopy` [#609](https://github.com/motdotla/node-lambda/pull/609) 466 | 467 | ## [1.2.0] - 2022-12-21 468 | ### Features 469 | - Add support for nodejs18x [#616](https://github.com/motdotla/node-lambda/pull/616) 470 | 471 | ## [1.2.1] - 2023-11-28 472 | ### Features 473 | - Add 'ap-southeast-3' to S3_LOCATION_POSSIBLE_VALUES [#621](https://github.com/motdotla/node-lambda/pull/621) 474 | 475 | ### Bugfixes 476 | - Fix minimatch import [#640](https://github.com/motdotla/node-lambda/pull/640) 477 | - Fix typo [#654](https://github.com/motdotla/node-lambda/pull/654) 478 | 479 | ### Minor changes 480 | - Improve testing 481 | - Update dependencies 482 | - Update README 483 | 484 | ## [1.3.0] - 2024-01-04 485 | ### Features 486 | - Support .mjs [#701](https://github.com/motdotla/node-lambda/pull/701) 487 | - Update lambda runtime list [#702](https://github.com/motdotla/node-lambda/pull/702) 488 | 489 | ### Minor changes 490 | - Update dependencies 491 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Scott Motte 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > If you found this useful, consider also checking out [dotenvx](https://github.com/dotenvx/dotenvx). Thank you! 😇 2 | 3 | # node-lambda 4 | 5 | ![node-lambda](../master/node-lambda.png?raw=true) 6 | 7 | Command line tool to locally run and deploy your node.js application to [Amazon Lambda](http://aws.amazon.com/lambda/). 8 | 9 | ![Node CI](https://github.com/motdotla/node-lambda/workflows/Node%20CI/badge.svg) 10 | [![NPM version](https://badge.fury.io/js/node-lambda.svg)](http://badge.fury.io/js/node-lambda) 11 | 12 | ``` 13 | $ node-lambda run 14 | ``` 15 | 16 | ## Installation 17 | 18 | With `npx`: 19 | 20 | ``` 21 | $ npx node-lambda@latest [COMMAND] 22 | ``` 23 | 24 | Globally installed: 25 | 26 | ``` 27 | $ npm install -g node-lambda 28 | ``` 29 | 30 | ## Example App 31 | 32 | Example apps make it easy to get up and running 33 | * [JavaScript example](https://github.com/motdotla/node-lambda-template). 34 | * [TypeScript example](https://github.com/fogfish/node-lambda-typescript-template). 35 | 36 | ## Usage 37 | 38 | There are 4 available commands. 39 | 40 | ``` 41 | $ npx node-lambda@latest setup 42 | $ npx node-lambda@latest run 43 | $ npx node-lambda@latest package 44 | $ npx node-lambda@latest deploy 45 | ``` 46 | 47 | ### Commands 48 | 49 | #### setup 50 | 51 | Initializes the `event.json`, `context.json`, `.env`, `deploy.env` files, and `event_sources.json` files. `event.json` is where you mock your event. `context.json` is where you can add additional mock data to the context passed to your lambda function. `.env` is where you place your deployment configuration. `deploy.env` has the same format as `.env`, but is used for holding any environment/config variables that you need to be deployed with your code to Lambda but you don't want in version control (e.g. DB connection info). `event_sources.json` is used to set the event source of the Lambda function (Not all event sources available in Lambda are supported). 52 | 53 | ``` 54 | $ npx node-lambda@latest setup --help 55 | 56 | Usage: setup [options] 57 | 58 | Sets up the .env file. 59 | 60 | 61 | Options: 62 | -h, --help output usage information 63 | -j, --eventFile [event.json] Event JSON File 64 | -x, --contextFile [context.json] Context JSON File 65 | ``` 66 | 67 | After running setup, it's a good idea to gitignore the generated `event.json` and `.env` files, as well as `.lambda`. 68 | 69 | ``` 70 | $ echo -e ".env\ndeploy.env\nevent.json\n.lambda" >> .gitignore 71 | ``` 72 | 73 | ##### Deploy env variables 74 | 75 | ``` 76 | PACKAGE_MANAGER // (default: 'npm') 77 | AWS_ENVIRONMENT // (default: '') 78 | AWS_ENDPOINT // (default: '') 79 | CONFIG_FILE // (default: '') 80 | EVENT_SOURCE_FILE // (default: '') 81 | EXCLUDE_GLOBS // (default: '') 82 | AWS_ACCESS_KEY_ID // (default: not set!) 83 | AWS_SECRET_ACCESS_KEY // (default: not set!) 84 | AWS_PROFILE = // (default: '') 85 | AWS_SESSION_TOKEN = // (default: '') 86 | AWS_REGION = // (default: 'us-east-1,us-west-2,eu-west-1') 87 | AWS_FUNCTION_NAME // (default: package.json.name or 'UnnamedFunction') 88 | AWS_HANDLER // (default: 'index.handler') 89 | AWS_ROLE_ARN || AWS_ROLE // (default: 'missing') 90 | AWS_MEMORY_SIZE // (default: 128) 91 | AWS_TIMEOUT // (default: 60) 92 | AWS_RUN_TIMEOUT // (default: 3) 93 | AWS_ARCHITECTURE // (default: 'x86_64') 94 | AWS_DESCRIPTION // (default: package.json.description or '') 95 | AWS_RUNTIME // (default: 'nodejs16.x') 96 | AWS_PUBLISH // (default: false) 97 | AWS_FUNCTION_VERSION // (default: '') 98 | AWS_VPC_SUBNETS // (default: '') 99 | AWS_VPC_SECURITY_GROUPS // (default: '') 100 | AWS_TRACING_CONFIG // (default: '') 101 | AWS_LAYERS // (default: '') 102 | AWS_LOGS_RETENTION_IN_DAYS // (default: '') 103 | EVENT_FILE // (default: 'event.json') 104 | PACKAGE_DIRECTORY // (default: not set) 105 | CONTEXT_FILE // (default: 'context.json') 106 | PREBUILT_DIRECTORY // (default: '') 107 | SRC_DIRECTORY // (default: '') 108 | DEPLOY_TIMEOUT // (default: '120000') 109 | DOCKER_IMAGE // (default: '') 110 | DEPLOY_ZIPFILE // (default: '') 111 | DEPLOY_USE_S3 // (default: false) 112 | IMAGE_URI // (default: '') 113 | AWS_DLQ_TARGET_ARN // (default: not set) 114 | AWS_TAGS // (default: '') 115 | ``` 116 | 117 | #### run 118 | 119 | Runs your Amazon Lambda index.js file locally. Passes `event.json` data to the Amazon Lambda event object. 120 | 121 | ``` 122 | $ npx node-lambda@latest run --help 123 | Usage: node-lambda run|execute [options] 124 | 125 | Run your Amazon Lambda application locally 126 | 127 | Options: 128 | -H, --handler [AWS_HANDLER] Lambda Handler {index.handler} (default: "index.handler") 129 | -j, --eventFile [EVENT_FILE] Event JSON File (default: "event.json") 130 | -u, --runtime [AWS_RUNTIME] Lambda Runtime (default: "nodejs16.x") 131 | -t, --timeout [AWS_RUN_TIMEOUT] Lambda Timeout (default: 3) 132 | -f, --configFile [CONFIG_FILE] Path to file holding secret environment variables (e.g. "deploy.env") (default: "") 133 | -x, --contextFile [CONTEXT_FILE] Context JSON File (default: "context.json") 134 | -M, --enableRunMultipleEvents [ENABLE_RUN_MULTIPLE_EVENTS] Enable run multiple events (default: true) 135 | -y, --proxy [PROXY] Proxy server (default: "") 136 | --apiGateway Convert to API Gateway events (default: false) 137 | -h, --help display help for command 138 | ``` 139 | 140 | #### package 141 | 142 | Bundles your application into a local zip file. 143 | 144 | ``` 145 | $ npx node-lambda@latest package --help 146 | Usage: node-lambda package|zip [options] 147 | 148 | Create zipped package for Amazon Lambda deployment 149 | 150 | Options: 151 | --packageManager [PACKAGE_MANAGER] Package manager used to install dependencies (default: "npm", options: "npm", "yarn") 152 | -A, --packageDirectory [PACKAGE_DIRECTORY] Local Package Directory 153 | -I, --dockerImage [DOCKER_IMAGE] Docker image for npm ci (default: "") 154 | -n, --functionName [AWS_FUNCTION_NAME] Lambda FunctionName (default: "node-lambda") 155 | -H, --handler [AWS_HANDLER] Lambda Handler {index.handler} (default: "index.handler") 156 | -e, --environment [AWS_ENVIRONMENT] Choose environment {dev, staging, production} (default: "") 157 | -x, --excludeGlobs [EXCLUDE_GLOBS] Space-separated glob pattern(s) for additional exclude files (e.g. 158 | "event.json dotenv.sample") (default: "") 159 | -D, --prebuiltDirectory [PREBUILT_DIRECTORY] Prebuilt directory (default: "") 160 | -m, --keepNodeModules [KEEP_NODE_MODULES] Keep the current node_modules directory. (default: false) 161 | -v, --dockerVolumes [DOCKER_VOLUMES] Additional docker volumes to mount. Each volume definition has to be 162 | separated by a space (e.g. "$HOME/.gitconfig:/etc/gitconfig 163 | $HOME/.ssh:/root/.ssh") (default: "") 164 | --no-optionalDependencies Run `npm install` with `--no-optional` 165 | -h, --help display help for command 166 | ``` 167 | 168 | #### deploy 169 | 170 | Bundles and deploys your application up to Amazon Lambda. 171 | 172 | ``` 173 | $ npx node-lambda@latest deploy --help 174 | Usage: node-lambda deploy [options] 175 | 176 | Deploy your application to Amazon Lambda 177 | 178 | Options: 179 | --packageManager [PACKAGE_MANAGER] Package manager used to install dependencies (default: "npm", options: "npm", "yarn") 180 | -e, --environment [AWS_ENVIRONMENT] Choose environment {dev, staging, production} (default: "") 181 | -E, --endpoint [AWS_ENDPOINT] Choose endpoint (e.g. localstack, "http://127.0.0.1:4574") (default: "") 182 | -a, --accessKey [AWS_ACCESS_KEY_ID] AWS Access Key 183 | -s, --secretKey [AWS_SECRET_ACCESS_KEY] AWS Secret Key 184 | -P, --profile [AWS_PROFILE] AWS Profile (default: "") 185 | -k, --sessionToken [AWS_SESSION_TOKEN] AWS Session Token (default: "") 186 | -r, --region [AWS_REGION] AWS Region (default: "us-east-1,us-west-2,eu-west-1") 187 | -n, --functionName [AWS_FUNCTION_NAME] Lambda FunctionName (default: "node-lambda") 188 | -H, --handler [AWS_HANDLER] Lambda Handler {index.handler} (default: "index.handler") 189 | -o, --role [AWS_ROLE] Amazon Role ARN (default: "missing") 190 | -m, --memorySize [AWS_MEMORY_SIZE] Lambda Memory Size (default: 128) 191 | -t, --timeout [AWS_TIMEOUT] Lambda Timeout (default: 60) 192 | --architecture [AWS_ARCHITECTURE] The instruction set architecture that the function supports. (x86_64|arm64) (default: "x86_64") 193 | -d, --description [AWS_DESCRIPTION] Lambda Description (default: "Command line tool for locally running and remotely deploying your node.js applications to Amazon Lambda.") 194 | -u, --runtime [AWS_RUNTIME] Lambda Runtime (default: "nodejs16.x") 195 | -p, --publish [AWS_PUBLISH] Lambda Publish (default: false) 196 | -L, --lambdaVersion [AWS_FUNCTION_VERSION] Lambda Function Version (default: "") 197 | -b, --vpcSubnets [AWS_VPC_SUBNETS] Lambda Function VPC Subnet IDs (comma delimited) (default: "") 198 | -g, --vpcSecurityGroups [AWS_VPC_SECURITY_GROUPS] Lambda VPC Security Group IDs (comma delimited) (default: "") 199 | -K, --kmsKeyArn [AWS_KMS_KEY_ARN] Lambda KMS Key ARN (default: "") 200 | -Q, --deadLetterConfigTargetArn [AWS_DLQ_TARGET_ARN] Lambda DLQ resource 201 | -c, --tracingConfig [AWS_TRACING_CONFIG] Lambda tracing settings (default: "") 202 | -l, --layers [AWS_LAYERS] Lambda Layers settings (e.g. "ARN1,ARN2[,..])" (default: "") 203 | -R, --retentionInDays [AWS_LOGS_RETENTION_IN_DAYS] CloudWatchLogs retentionInDays settings (default: "") 204 | -G, --sourceDirectory [SRC_DIRECTORY] Path to lambda source Directory (e.g. "./some-lambda") (default: "") 205 | -I, --dockerImage [DOCKER_IMAGE] Docker image for npm ci (default: "") 206 | -f, --configFile [CONFIG_FILE] Path to file holding secret environment variables (e.g. "deploy.env") (default: "") 207 | -S, --eventSourceFile [EVENT_SOURCE_FILE] Path to file holding event source mapping variables (e.g. "event_sources.json") (default: "") 208 | -x, --excludeGlobs [EXCLUDE_GLOBS] Space-separated glob pattern(s) for additional exclude files (e.g. "event.json dotenv.sample") (default: "") 209 | -D, --prebuiltDirectory [PREBUILT_DIRECTORY] Prebuilt directory (default: "") 210 | -T, --deployTimeout [DEPLOY_TIMEOUT] Deploy Timeout (default: 120000) 211 | -z, --deployZipfile [DEPLOY_ZIPFILE] Deploy zipfile (default: "") 212 | -B, --deployUseS3 [DEPLOY_USE_S3] Use S3 to deploy. (default: false) 213 | -i, --imageUri [IMAGE_URI] URI of a container image in the Amazon ECR registry. (default: "") 214 | -y, --proxy [PROXY] Proxy server (default: "") 215 | -A, --tags [AWS_TAGS] Tags as key value pairs (e.g. "tagname1=tagvalue1,tagname2=tagvalue2)" (default: "") 216 | --silent Silent or quiet mode (default: false) 217 | --no-optionalDependencies Run `npm install` with `--no-optional` 218 | -h, --help display help for command 219 | ``` 220 | 221 | If you are deploying to a custom endpoint you may also need to pass in an access key/secret. For localstack these can be anything, but cannot be blank: 222 | 223 | ``` 224 | npx node-lambda@latest deploy --endpoint http://localhost:4574 --accessKey '1234' --secretKey '1234' 225 | ``` 226 | 227 | ## Custom Environment Variables 228 | 229 | AWS Lambda will let you set environment variables for your function. Use the sample `deploy.env` file in combination with the `--configFile` flag to set values which will be added to the lambda configuration upon deploy. Environment variables will also be set when running locally using the same flag 230 | 231 | ## Node.js Runtime Configuration 232 | 233 | AWS Lambda now supports Node.js 20, Node.js 18 and Node.js 16. Please also check the [Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) page. 234 | 235 | ## Use S3 to deploy 236 | 237 | Use the command line argument `--deployUseS3` or `-B`. (This option is true/false.) 238 | Example: `% npx node-lambda@latest deploy -B` 239 | 240 | You can also set the environment variable `DEPLOY_USE_S3`. 241 | Example: `DEPLOY_USE_S3=true` 242 | 243 | Use the environment variable to set the bucket name or S3 key prefix. 244 | The environment variable name is different for each region. Please set it to the environment variable you want to deploy. 245 | It can be set in `.env`. 246 | 247 | Example: 248 | 249 | ```sh 250 | # S3__BUCKET 251 | S3_US_WEST_1_BUCKET=test_aaa 252 | 253 | # S3__PREFIX 254 | S3_US_WEST_1_PREFIX=bbb 255 | ``` 256 | 257 | In this case, the S3 key: 258 | `test_aaa/bbb/deploy-package-${FunctionName}.zip` 259 | 260 | ## To deploy a container image to Lambda 261 | 262 | 1. [Pushing a Docker image to ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html) 263 | 2. Specify the Image URI of the ECR with the environment variable:`IMAGE_URI` or `--imageUri` option, and deploy. 264 | 265 | ## Post install script 266 | When running `node-lambda deploy` if you need to do some action after `npm install --production` and before deploying to AWS Lambda (e.g. replace some modules with precompiled ones or download some libraries, replace some config file depending on environment) you can create `post_install.sh` script. If the file exists the script will be executed (and output shown after execution) if not it is skipped. Environment string is passed to script as first parameter so you can use it if needed. Make sure that the script is executable. 267 | 268 | Example `post_install.sh`: 269 | ``` 270 | printf "\n\n###### Post install script ###### \n" 271 | ENV="production"; 272 | if [ ! -z $1 ] 273 | then 274 | ENV=$1; 275 | fi 276 | cp -v "config_$ENV.js" "config.js" \ 277 | && printf "###### DONE! ###### \n\n" 278 | ``` 279 | 280 | ## Post invoke script (example) 281 | If you wish to invoke your deployed AWS Lambda function, you can add the following as a `script` to your `package.json`: 282 | 283 | ``` 284 | "invoke:remote": "aws lambda invoke --function-name myLambdaFnName --payload fileb://fixtures/hi.json invoked.json --log-type Tail | jq -r '.LogResult' | base64 --decode && rm invoked.json" 285 | ``` 286 | 287 | ## Prebuilt packages 288 | The `--prebuiltDirectory` flag is useful for working with Webpack for example. It skips `npm install --production` and `post_install.sh` and simply packages the specified directory. 289 | 290 | ## Handling `npm link` and Dependencies With Local Paths 291 | Perhaps the easiest way to handle these cases is to bundle the code using Webpack and use the `--prebuiltDirectory` flag to package the output for deployment. 292 | 293 | ## ScheduleEvents 294 | #### Optional Parameter 295 | When using the eventSourceFile flag (-S or --eventSourceFile) to set a ScheduleEvent trigger, you can pass an optional _ScheduleDescription_ key into the ScheduleEvent object with a custom description for the CloudWatch event rule you are defining. By default, node-lambda generates a _ScheduleDescription_ for you based on the ScheduleName and ScheduleExpression of the rule. 296 | 297 | #### Note on ScheduleState for ScheduleEvents 298 | When setting ScheduleState to `ENABLED` or `DISABLED` for ScheduleEvents, it is useful to note that this sets the state of the CloudWatch Event rule but _DOES NOT_ set the state of the trigger for the Lambda function you are deploying; ScheduleEvent triggers are enabled by default in the Lambda console when added using the eventSourceFile flag. 299 | 300 | #### Known Issue 301 | ###### Duplicate ScheduleEvent Triggers 302 | If you are adding a trigger via the `eventSourceFile` for the first time, remove preexisting triggers from the Lambda console before deploying. Deploying a Lambda with the `--eventSourceFile` flag will *NOT* overwrite the same triggers created from the AWS console and may result in a duplicate triggers for the same rule. 303 | 304 | ## Contributing 305 | 306 | 1. Fork it 307 | 2. Create your feature branch (`git checkout -b my-new-feature`) 308 | 3. Commit your changes (`git commit -am 'Added some feature'`) 309 | 4. Push to the branch (`git push origin my-new-feature`) 310 | 5. Create new Pull Request 311 | 312 | ### Coding style 313 | This project uses [JavaScript Standard Style](https://standardjs.com/). 314 | 315 | [![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 316 | 317 | ## Running tests 318 | 319 | ``` 320 | $ npm install 321 | $ npm test 322 | ``` 323 | -------------------------------------------------------------------------------- /bin/node-lambda: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | require('@dotenvx/dotenvx').config() 6 | const process = require('process') 7 | const path = require('path') 8 | 9 | const lambda = require(path.join(__dirname, '..', 'lib', 'main.js')) 10 | const { program } = require('commander') 11 | const fs = require('fs') 12 | const packageJson = fs.existsSync(path.join(process.cwd(), 'package.json')) ? require(path.join(process.cwd(), 'package.json')) : {} 13 | const packageJsonName = packageJson.name || 'UnnamedFunction' 14 | 15 | const PACKAGE_MANAGER = process.env.PACKAGE_MANAGER || 'npm' 16 | const AWS_ENVIRONMENT = process.env.AWS_ENVIRONMENT || '' 17 | const AWS_ENDPOINT = process.env.AWS_ENDPOINT || '' 18 | const CONFIG_FILE = process.env.CONFIG_FILE || '' 19 | const EVENT_SOURCE_FILE = process.env.EVENT_SOURCE_FILE || '' 20 | const EXCLUDE_GLOBS = process.env.EXCLUDE_GLOBS || '' 21 | const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID 22 | const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY 23 | const AWS_PROFILE = process.env.AWS_PROFILE || '' 24 | const AWS_SESSION_TOKEN = process.env.AWS_SESSION_TOKEN || '' 25 | const AWS_REGION = process.env.AWS_REGION || 'us-east-1,us-west-2,eu-west-1' 26 | const AWS_FUNCTION_NAME = process.env.AWS_FUNCTION_NAME || packageJsonName 27 | const AWS_HANDLER = process.env.AWS_HANDLER || 'index.handler' 28 | const AWS_ROLE = process.env.AWS_ROLE_ARN || process.env.AWS_ROLE || 'missing' 29 | const AWS_MEMORY_SIZE = process.env.AWS_MEMORY_SIZE || 128 30 | const AWS_TIMEOUT = process.env.AWS_TIMEOUT || 60 31 | const AWS_RUN_TIMEOUT = process.env.AWS_RUN_TIMEOUT || 3 32 | const AWS_ARCHITECTURE = process.env.AWS_ARCHITECTURE || 'x86_64' 33 | const AWS_DESCRIPTION = process.env.AWS_DESCRIPTION || packageJson.description || '' 34 | const AWS_RUNTIME = process.env.AWS_RUNTIME || 'nodejs16.x' 35 | const AWS_PUBLISH = process.env.AWS_PUBLISH || false 36 | const AWS_FUNCTION_VERSION = process.env.AWS_FUNCTION_VERSION || '' 37 | const AWS_VPC_SUBNETS = process.env.AWS_VPC_SUBNETS || '' 38 | const AWS_VPC_SECURITY_GROUPS = process.env.AWS_VPC_SECURITY_GROUPS || '' 39 | const AWS_TRACING_CONFIG = process.env.AWS_TRACING_CONFIG || '' 40 | const AWS_LAYERS = process.env.AWS_LAYERS || '' 41 | const AWS_LOGS_RETENTION_IN_DAYS = process.env.AWS_LOGS_RETENTION_IN_DAYS || '' 42 | const EVENT_FILE = process.env.EVENT_FILE || 'event.json' 43 | const PACKAGE_DIRECTORY = process.env.PACKAGE_DIRECTORY 44 | const CONTEXT_FILE = process.env.CONTEXT_FILE || 'context.json' 45 | const PREBUILT_DIRECTORY = process.env.PREBUILT_DIRECTORY || '' 46 | const SRC_DIRECTORY = process.env.SRC_DIRECTORY || '' 47 | const DEPLOY_TIMEOUT = process.env.DEPLOY_TIMEOUT || 120000 48 | const DOCKER_IMAGE = process.env.DOCKER_IMAGE || '' 49 | const DEPLOY_ZIPFILE = process.env.DEPLOY_ZIPFILE || '' 50 | const DEPLOY_USE_S3 = process.env.DEPLOY_USE_S3 || false 51 | const AWS_KMS_KEY_ARN = process.env.AWS_KMS_KEY_ARN || '' 52 | const AWS_DLQ_TARGET_ARN = (() => { 53 | // You can clear the setting by passing an empty string 54 | // when executing updateFunctionConfiguration 55 | if (process.env.AWS_DLQ_TARGET_ARN !== undefined) { 56 | return process.env.AWS_DLQ_TARGET_ARN 57 | } 58 | return undefined 59 | })() 60 | const PROXY = process.env.PROXY || process.env.http_proxy || '' 61 | const ENABLE_RUN_MULTIPLE_EVENTS = true 62 | const KEEP_NODE_MODULES = process.env.KEEP_NODE_MODULES || false 63 | const DOCKER_VOLUMES = process.env.DOCKER_VOLUMES || '' 64 | const AWS_TAGS = process.env.AWS_TAGS || '' 65 | const IMAGE_URI = process.env.IMAGE_URI || '' 66 | 67 | program 68 | .command('deploy') 69 | .description('Deploy your application to Amazon Lambda') 70 | .option('--packageManager [PACKAGE_MANAGER]', 'Package manager used to install dependencies', PACKAGE_MANAGER) 71 | .option('-e, --environment [AWS_ENVIRONMENT]', 'Choose environment {dev, staging, production}', 72 | AWS_ENVIRONMENT) 73 | .option('-E, --endpoint [AWS_ENDPOINT]', 'Choose endpoint (e.g. localstack, "http://127.0.0.1:4574")', 74 | AWS_ENDPOINT) 75 | .option('-a, --accessKey [AWS_ACCESS_KEY_ID]', 'AWS Access Key', AWS_ACCESS_KEY_ID) 76 | .option('-s, --secretKey [AWS_SECRET_ACCESS_KEY]', 'AWS Secret Key', AWS_SECRET_ACCESS_KEY) 77 | .option('-P, --profile [AWS_PROFILE]', 'AWS Profile', AWS_PROFILE) 78 | .option('-k, --sessionToken [AWS_SESSION_TOKEN]', 'AWS Session Token', AWS_SESSION_TOKEN) 79 | .option('-r, --region [AWS_REGION]', 'AWS Region', AWS_REGION) 80 | .option('-n, --functionName [AWS_FUNCTION_NAME]', 'Lambda FunctionName', AWS_FUNCTION_NAME) 81 | .option('-H, --handler [AWS_HANDLER]', 'Lambda Handler {index.handler}', AWS_HANDLER) 82 | .option('-o, --role [AWS_ROLE]', 'Amazon Role ARN', AWS_ROLE) 83 | .option('-m, --memorySize [AWS_MEMORY_SIZE]', 'Lambda Memory Size', AWS_MEMORY_SIZE) 84 | .option('-t, --timeout [AWS_TIMEOUT]', 'Lambda Timeout', AWS_TIMEOUT) 85 | .option('--architecture [AWS_ARCHITECTURE]', 'The instruction set architecture that the function supports. (x86_64|arm64)', AWS_ARCHITECTURE) 86 | .option('-d, --description [AWS_DESCRIPTION]', 'Lambda Description', AWS_DESCRIPTION) 87 | .option('-u, --runtime [AWS_RUNTIME]', 'Lambda Runtime', AWS_RUNTIME) 88 | .option('-p, --publish [AWS_PUBLISH]', 'Lambda Publish', AWS_PUBLISH) 89 | .option('-L, --lambdaVersion [AWS_FUNCTION_VERSION]', 'Lambda Function Version', AWS_FUNCTION_VERSION) 90 | .option('-b, --vpcSubnets [AWS_VPC_SUBNETS]', 'Lambda Function VPC Subnet IDs (comma delimited)', AWS_VPC_SUBNETS) 91 | .option('-g, --vpcSecurityGroups [AWS_VPC_SECURITY_GROUPS]', 'Lambda VPC Security Group IDs (comma delimited)', 92 | AWS_VPC_SECURITY_GROUPS) 93 | .option('-K, --kmsKeyArn [AWS_KMS_KEY_ARN]', 'Lambda KMS Key ARN', AWS_KMS_KEY_ARN) 94 | .option('-Q, --deadLetterConfigTargetArn [AWS_DLQ_TARGET_ARN]', 'Lambda DLQ resource', 95 | AWS_DLQ_TARGET_ARN) 96 | .option('-c, --tracingConfig [AWS_TRACING_CONFIG]', 'Lambda tracing settings', 97 | AWS_TRACING_CONFIG) 98 | .option('-l, --layers [AWS_LAYERS]', 'Lambda Layers settings (e.g. "ARN1,ARN2[,..])"', AWS_LAYERS) 99 | .option('-R, --retentionInDays [AWS_LOGS_RETENTION_IN_DAYS]', 'CloudWatchLogs retentionInDays settings', 100 | AWS_LOGS_RETENTION_IN_DAYS) 101 | .option('-G, --sourceDirectory [SRC_DIRECTORY]', 'Path to lambda source Directory (e.g. "./some-lambda")', SRC_DIRECTORY) 102 | .option('-I, --dockerImage [DOCKER_IMAGE]', 'Docker image for npm ci', DOCKER_IMAGE) 103 | .option('-f, --configFile [CONFIG_FILE]', 104 | 'Path to file holding secret environment variables (e.g. "deploy.env")', CONFIG_FILE) 105 | .option('-S, --eventSourceFile [EVENT_SOURCE_FILE]', 106 | 'Path to file holding event source mapping variables (e.g. "event_sources.json")', EVENT_SOURCE_FILE) 107 | .option('-x, --excludeGlobs [EXCLUDE_GLOBS]', 108 | 'Space-separated glob pattern(s) for additional exclude files (e.g. "event.json dotenv.sample")', EXCLUDE_GLOBS) 109 | .option('-D, --prebuiltDirectory [PREBUILT_DIRECTORY]', 'Prebuilt directory', PREBUILT_DIRECTORY) 110 | .option('-T, --deployTimeout [DEPLOY_TIMEOUT]', 'Deploy Timeout', DEPLOY_TIMEOUT) 111 | .option('-z, --deployZipfile [DEPLOY_ZIPFILE]', 'Deploy zipfile', DEPLOY_ZIPFILE) 112 | .option('-B, --deployUseS3 [DEPLOY_USE_S3]', 'Use S3 to deploy.', DEPLOY_USE_S3) 113 | .option('-i, --imageUri [IMAGE_URI]', 'URI of a container image in the Amazon ECR registry.', IMAGE_URI) 114 | .option('-y, --proxy [PROXY]', 'Proxy server', PROXY) 115 | .option('-A, --tags [AWS_TAGS]', 'Tags as key value pairs (e.g. "tagname1=tagvalue1,tagname2=tagvalue2)"', AWS_TAGS) 116 | .option('--silent', 'Silent or quiet mode', false) 117 | .option('--no-optionalDependencies', 'Run `npm install` with `--no-optional`') 118 | .action((prg) => { 119 | if (prg.silent) { 120 | console.log = () => {} 121 | } 122 | lambda.deploy(prg) 123 | }) 124 | 125 | program 126 | .command('package') 127 | .alias('zip') 128 | .description('Create zipped package for Amazon Lambda deployment') 129 | .option('--packageManager [PACKAGE_MANAGER]', 'Package manager used to install dependencies', PACKAGE_MANAGER) 130 | .option('-A, --packageDirectory [PACKAGE_DIRECTORY]', 'Local Package Directory', PACKAGE_DIRECTORY) 131 | .option('-I, --dockerImage [DOCKER_IMAGE]', 'Docker image for npm ci', DOCKER_IMAGE) 132 | .option('-n, --functionName [AWS_FUNCTION_NAME]', 'Lambda FunctionName', AWS_FUNCTION_NAME) 133 | .option('-H, --handler [AWS_HANDLER]', 'Lambda Handler {index.handler}', AWS_HANDLER) 134 | .option('-e, --environment [AWS_ENVIRONMENT]', 'Choose environment {dev, staging, production}', 135 | AWS_ENVIRONMENT) 136 | .option('-x, --excludeGlobs [EXCLUDE_GLOBS]', 137 | 'Space-separated glob pattern(s) for additional exclude files (e.g. "event.json dotenv.sample")', EXCLUDE_GLOBS) 138 | .option('-D, --prebuiltDirectory [PREBUILT_DIRECTORY]', 'Prebuilt directory', PREBUILT_DIRECTORY) 139 | .option('-m, --keepNodeModules [KEEP_NODE_MODULES]', 'Keep the current node_modules directory.', KEEP_NODE_MODULES) 140 | .option('-v, --dockerVolumes [DOCKER_VOLUMES]', 'Additional docker volumes to mount. Each volume definition has to be separated by a space (e.g. "$HOME/.gitconfig:/etc/gitconfig $HOME/.ssh:/root/.ssh")', DOCKER_VOLUMES) 141 | .option('--no-optionalDependencies', 'Run `npm install` with `--no-optional`') 142 | .action((prg) => lambda.package(prg)) 143 | 144 | program 145 | .command('run') 146 | .alias('execute') 147 | .description('Run your Amazon Lambda application locally') 148 | .option('-H, --handler [AWS_HANDLER]', 'Lambda Handler {index.handler}', AWS_HANDLER) 149 | .option('-j, --eventFile [EVENT_FILE]', 'Event JSON File', EVENT_FILE) 150 | .option('-u, --runtime [AWS_RUNTIME]', 'Lambda Runtime', AWS_RUNTIME) 151 | .option('-t, --timeout [AWS_RUN_TIMEOUT]', 'Lambda Timeout', AWS_RUN_TIMEOUT) 152 | .option('-f, --configFile [CONFIG_FILE]', 153 | 'Path to file holding secret environment variables (e.g. "deploy.env")', CONFIG_FILE) 154 | .option('-x, --contextFile [CONTEXT_FILE]', 'Context JSON File', CONTEXT_FILE) 155 | .option('-M, --enableRunMultipleEvents [ENABLE_RUN_MULTIPLE_EVENTS]', 'Enable run multiple events', 156 | ENABLE_RUN_MULTIPLE_EVENTS) 157 | .option('-y, --proxy [PROXY]', 'Proxy server', PROXY) 158 | .option('--apiGateway', 'Convert to API Gateway events', false) 159 | .action((prg) => lambda.run(prg)) 160 | 161 | program 162 | .command('setup') 163 | .description('Sets up the .env file.') 164 | .option('-j, --eventFile [EVENT_FILE]', 'Event JSON File', EVENT_FILE) 165 | .option('-x, --contextFile [CONTEXT_FILE]', 'Context JSON File', CONTEXT_FILE) 166 | .action((prg) => lambda.setup(prg)) 167 | 168 | program 169 | .version(lambda.version) 170 | .parse(process.argv) 171 | 172 | if (!process.argv.slice(2).length) { 173 | program.outputHelp() 174 | } 175 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // For development/testing purposes 2 | exports.handler = (event, context, callback) => { 3 | console.log('Running index.handler') 4 | console.log('==================================') 5 | console.log('event', event) 6 | console.log('==================================') 7 | console.log('Stopping index.handler') 8 | callback(null) 9 | } 10 | -------------------------------------------------------------------------------- /index_mjs.mjs: -------------------------------------------------------------------------------- 1 | // For development/testing purposes 2 | export const handler = (event, context, callback) => { 3 | console.log('Running index.handler (mjs)') 4 | console.log('==================================') 5 | console.log('event', event) 6 | console.log('==================================') 7 | console.log('Stopping index.handler (mjs)') 8 | callback(null) 9 | } 10 | -------------------------------------------------------------------------------- /lib/.env.example: -------------------------------------------------------------------------------- 1 | AWS_ENVIRONMENT=development 2 | AWS_ACCESS_KEY_ID=your_key 3 | AWS_SECRET_ACCESS_KEY=your_secret 4 | AWS_PROFILE= 5 | AWS_SESSION_TOKEN= 6 | AWS_ROLE_ARN=your_amazon_role 7 | AWS_REGION=us-east-1 8 | AWS_FUNCTION_NAME= 9 | AWS_HANDLER=index.handler 10 | AWS_MEMORY_SIZE=128 11 | AWS_TIMEOUT=3 12 | AWS_DESCRIPTION= 13 | AWS_RUNTIME=nodejs20.x 14 | AWS_VPC_SUBNETS= 15 | AWS_VPC_SECURITY_GROUPS= 16 | AWS_TRACING_CONFIG= 17 | AWS_LOGS_RETENTION_IN_DAYS= 18 | EXCLUDE_GLOBS="event.json build/" 19 | CONFIG_FILE="deploy.env" 20 | PACKAGE_DIRECTORY=build 21 | -------------------------------------------------------------------------------- /lib/aws.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const aws = require('aws-sdk') 4 | const proxy = require('proxy-agent') 5 | 6 | module.exports = { 7 | sdk: aws, 8 | updateConfig (config, region) { 9 | const awsSecurity = { region } 10 | 11 | if (config.profile) { 12 | aws.config.credentials = new aws.SharedIniFileCredentials({ 13 | profile: config.profile 14 | }) 15 | } else { 16 | awsSecurity.accessKeyId = config.accessKey 17 | awsSecurity.secretAccessKey = config.secretKey 18 | } 19 | 20 | if (config.sessionToken) { 21 | awsSecurity.sessionToken = config.sessionToken 22 | } 23 | 24 | if (config.deployTimeout) { 25 | aws.config.httpOptions.timeout = parseInt(config.deployTimeout) 26 | } 27 | 28 | if (config.proxy) { 29 | aws.config.httpOptions.agent = proxy(config.proxy) 30 | } 31 | 32 | if (config.endpoint) { 33 | aws.config.endpoint = config.endpoint 34 | } 35 | 36 | aws.config.update(awsSecurity) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/cloudwatch_logs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class CloudWatchLogs { 4 | constructor (aws, region) { 5 | // Authenticated `aws` object in `lib/main.js` 6 | this.lambda = new aws.Lambda({ 7 | region, 8 | apiVersion: '2015-03-31' 9 | }) 10 | this.cloudwatchlogs = new aws.CloudWatchLogs({ 11 | apiVersion: '2014-03-28' 12 | }) 13 | } 14 | 15 | _logGroupName (params) { 16 | return `/aws/lambda/${params.FunctionName}` 17 | } 18 | 19 | _createLogGroup (params) { 20 | return new Promise((resolve, reject) => { 21 | this.cloudwatchlogs.createLogGroup({ 22 | logGroupName: this._logGroupName(params) 23 | }, (err, data) => { 24 | if (err) { 25 | if (err.code === 'ResourceAlreadyExistsException') { 26 | // If it exists it will result in an error but there is no problem. 27 | return resolve({}) 28 | } 29 | return reject(err) 30 | } 31 | 32 | resolve(data) 33 | }) 34 | }) 35 | } 36 | 37 | _putRetentionPolicy (params) { 38 | return new Promise((resolve, reject) => { 39 | this.cloudwatchlogs.putRetentionPolicy({ 40 | logGroupName: this._logGroupName(params), 41 | retentionInDays: params.retentionInDays 42 | }, (err, data) => { 43 | if (err) return reject(err) 44 | resolve(data) 45 | }) 46 | }) 47 | } 48 | 49 | setLogsRetentionPolicy (params) { 50 | return this._createLogGroup(params) 51 | .then(() => this._putRetentionPolicy(params)) 52 | } 53 | } 54 | 55 | module.exports = CloudWatchLogs 56 | -------------------------------------------------------------------------------- /lib/context.json.example: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /lib/deploy.env.example: -------------------------------------------------------------------------------- 1 | SECRET_VARIABLE=mysecretval 2 | -------------------------------------------------------------------------------- /lib/event.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value", 3 | "key2": "value2", 4 | "other_key": "other_value" 5 | } 6 | -------------------------------------------------------------------------------- /lib/event_sources.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "EventSourceMappings": [ 3 | { 4 | "EventSourceArn": "your event source arn", 5 | "StartingPosition": "LATEST", 6 | "BatchSize": 100, 7 | "Enabled": true 8 | } 9 | ], 10 | "ScheduleEvents": [ 11 | { 12 | "ScheduleName": "node-lambda-test-schedule", 13 | "ScheduleState": "ENABLED", 14 | "ScheduleExpression": "rate(1 hour)", 15 | "Input": 16 | { 17 | "key1": "value", 18 | "key2": "value" 19 | } 20 | } 21 | ], 22 | "S3Events": [{ 23 | "Bucket": "BUCKET_NAME", 24 | "Events": [ 25 | "s3:ObjectCreated:*" 26 | ], 27 | "Filter": { 28 | "Key": { 29 | "FilterRules": [{ 30 | "Name": "prefix", 31 | "Value": "STRING_VALUE" 32 | }] 33 | } 34 | } 35 | }] 36 | } 37 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const process = require('process') 4 | const path = require('path') 5 | const os = require('os') 6 | const aws = require(path.join(__dirname, 'aws')) 7 | const { exec, execSync, execFile } = require('child_process') 8 | const fs = require('fs-extra') 9 | const klaw = require('klaw') 10 | const packageJson = require(path.join(__dirname, '..', 'package.json')) 11 | const { minimatch } = require('minimatch') 12 | const archiver = require('archiver') 13 | const dotenv = require('@dotenvx/dotenvx') 14 | const ScheduleEvents = require(path.join(__dirname, 'schedule_events')) 15 | const S3Events = require(path.join(__dirname, 's3_events')) 16 | const S3Deploy = require(path.join(__dirname, 's3_deploy')) 17 | const CloudWatchLogs = require(path.join(__dirname, 'cloudwatch_logs')) 18 | 19 | const AWSXRay = require('aws-xray-sdk-core') 20 | const { createNamespace } = require('continuation-local-storage') 21 | 22 | const maxBufferSize = 50 * 1024 * 1024 23 | 24 | class Lambda { 25 | constructor () { 26 | this.version = packageJson.version 27 | } 28 | 29 | _createSampleFile (file, boilerplateName) { 30 | const exampleFile = path.join(process.cwd(), file) 31 | const boilerplateFile = path.join( 32 | __dirname, 33 | (boilerplateName || file) + '.example' 34 | ) 35 | 36 | if (!fs.existsSync(exampleFile)) { 37 | fs.writeFileSync(exampleFile, fs.readFileSync(boilerplateFile)) 38 | console.log(exampleFile + ' file successfully created') 39 | } 40 | } 41 | 42 | setup (program) { 43 | console.log('Running setup.') 44 | this._createSampleFile('.env', '.env') 45 | this._createSampleFile(program.eventFile, 'event.json') 46 | this._createSampleFile('deploy.env', 'deploy.env') 47 | this._createSampleFile(program.contextFile, 'context.json') 48 | this._createSampleFile('event_sources.json', 'event_sources.json') 49 | console.log(`Setup done. 50 | Edit the .env, deploy.env, ${program.contextFile}, \ 51 | event_sources.json and ${program.eventFile} files as needed.`) 52 | } 53 | 54 | async run (program) { 55 | if (!['nodejs16.x', 'nodejs18.x', 'nodejs20.x'].includes(program.runtime)) { 56 | console.error(`Runtime [${program.runtime}] is not supported.`) 57 | process.exit(254) 58 | } 59 | 60 | this._createSampleFile(program.eventFile, 'event.json') 61 | const splitHandler = program.handler.split('.') 62 | const filename = (() => { 63 | for (const extension of ['.js', '.mjs']) { 64 | if (fs.existsSync(splitHandler[0] + extension)) { 65 | return splitHandler[0] + extension 66 | } 67 | } 68 | })() 69 | if (filename == null) { 70 | console.error('Handler file not found.') 71 | process.exitCode = 255 72 | return 73 | } 74 | const handlername = splitHandler[1] 75 | 76 | // Set custom environment variables if program.configFile is defined 77 | if (program.configFile) { 78 | this._setRunTimeEnvironmentVars(program) 79 | } 80 | 81 | const handlerFilePath = (() => { 82 | const filePath = path.join(process.cwd(), filename) 83 | if (path.sep === '\\') { 84 | // Convert because of error in Windows. 85 | // Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: 86 | // Only URLs with a scheme in: file, data are supported by the default ESM loader. 87 | // On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:' 88 | return 'file:///' + filePath.split(path.sep).join('/') 89 | } 90 | return filePath 91 | })() 92 | const handler = (await import(handlerFilePath))[handlername] 93 | const event = require(path.join(process.cwd(), program.eventFile)) 94 | const context = require(path.join(process.cwd(), program.contextFile)) 95 | const enableRunMultipleEvents = (() => { 96 | if (typeof program.enableRunMultipleEvents === 'boolean') { 97 | return program.enableRunMultipleEvents 98 | } 99 | return program.enableRunMultipleEvents === 'true' 100 | })() 101 | 102 | if (Array.isArray(event) && enableRunMultipleEvents === true) { 103 | return this._runMultipleHandlers(event) 104 | } 105 | context.local = true 106 | const eventObject = (() => { 107 | if (program.apiGateway) { 108 | return this._convertToApiGatewayEvents(event) 109 | } 110 | return event 111 | })() 112 | this._runHandler(handler, eventObject, program, context) 113 | } 114 | 115 | _runHandler (handler, event, program, context) { 116 | const startTime = new Date() 117 | const timeout = Math.min(program.timeout, 900) * 1000 // convert the timeout into milliseconds 118 | 119 | const callback = (err, result) => { 120 | if (err) { 121 | process.exitCode = 255 122 | console.log('Error: ' + err) 123 | } else { 124 | process.exitCode = 0 125 | console.log('Success:') 126 | if (result) { 127 | console.log(JSON.stringify(result)) 128 | } 129 | } 130 | if (context.callbackWaitsForEmptyEventLoop === false) { 131 | process.exit() 132 | } 133 | } 134 | 135 | context.getRemainingTimeInMillis = () => { 136 | const currentTime = new Date() 137 | return timeout - (currentTime - startTime) 138 | } 139 | 140 | // The following three functions are deprecated in AWS Lambda. 141 | // Since it is sometimes used by other SDK, 142 | // it is a simple one that does not result in `not function` error 143 | context.succeed = (result) => console.log(JSON.stringify(result)) 144 | context.fail = (error) => console.log(JSON.stringify(error)) 145 | context.done = (error, results) => { 146 | console.log(JSON.stringify(error)) 147 | console.log(JSON.stringify(results)) 148 | } 149 | 150 | const nameSpace = createNamespace('AWSXRay') 151 | nameSpace.run(() => { 152 | nameSpace.set('segment', new AWSXRay.Segment('annotations')) 153 | const result = handler(event, context, callback) 154 | if (result != null) { 155 | Promise.resolve(result).then( 156 | resolved => { 157 | console.log('Result:') 158 | console.log(JSON.stringify(resolved)) 159 | }, 160 | rejected => { 161 | console.log('Error:') 162 | console.log(rejected) 163 | } 164 | ) 165 | } 166 | }) 167 | } 168 | 169 | _runMultipleHandlers (events) { 170 | console.log(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 171 | Usually you will receive a single Object from AWS Lambda. 172 | We added support for event.json to contain an array, 173 | so you can easily test run multiple events. 174 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 175 | `) 176 | 177 | const _argv = process.argv 178 | const eventFileOptionIndex = (() => { 179 | const index = _argv.indexOf('-j') 180 | if (index >= 0) return index 181 | return _argv.indexOf('--eventFile') 182 | })() 183 | _argv[0] = 'node' // For Windows support 184 | 185 | // In order to reproduce the logic of callbackWaitsForEmptyEventLoop, 186 | // we are going to execute `node-lambda run`. 187 | events.forEach((event, i) => { 188 | const tmpEventFile = `.${i}_tmp_event.json` 189 | const command = () => { 190 | if (eventFileOptionIndex === -1) { 191 | return _argv.concat(['-j', tmpEventFile]).join(' ') 192 | } 193 | _argv[eventFileOptionIndex + 1] = tmpEventFile 194 | return _argv.join(' ') 195 | } 196 | 197 | fs.writeFileSync(tmpEventFile, JSON.stringify(event)) 198 | const stdout = execSync(command(), { 199 | maxBuffer: maxBufferSize, 200 | env: process.env 201 | }) 202 | console.log('>>> Event:', event, '<<<') 203 | console.log(stdout.toString()) 204 | fs.unlinkSync(tmpEventFile) 205 | }) 206 | } 207 | 208 | _convertToApiGatewayEvents (event) { 209 | console.log(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 210 | Emulate only the body of the API Gateway event. 211 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 212 | `) 213 | return { 214 | body: JSON.stringify(event) 215 | } 216 | } 217 | 218 | _isUseS3 (program) { 219 | if (typeof program.deployUseS3 === 'boolean') { 220 | return program.deployUseS3 221 | } 222 | return program.deployUseS3 === 'true' 223 | } 224 | 225 | _useECR (program) { 226 | return program.imageUri != null && program.imageUri.length > 0 227 | } 228 | 229 | _params (program, buffer) { 230 | const params = { 231 | FunctionName: program.functionName + 232 | (program.environment ? '-' + program.environment : '') + 233 | (program.lambdaVersion ? '-' + program.lambdaVersion : ''), 234 | Code: {}, 235 | Handler: program.handler, 236 | Role: program.role, 237 | Runtime: program.runtime, 238 | Description: program.description, 239 | MemorySize: program.memorySize, 240 | Timeout: program.timeout, 241 | Architectures: program.architecture ? [program.architecture] : ['x86_64'], 242 | Publish: (() => { 243 | if (typeof program.publish === 'boolean') { 244 | return program.publish 245 | } 246 | return program.publish === 'true' 247 | })(), 248 | VpcConfig: { 249 | SubnetIds: [], 250 | SecurityGroupIds: [] 251 | }, 252 | Environment: { 253 | Variables: null 254 | }, 255 | KMSKeyArn: program.kmsKeyArn, 256 | DeadLetterConfig: { 257 | TargetArn: null 258 | }, 259 | TracingConfig: { 260 | Mode: null 261 | }, 262 | Layers: [], 263 | Tags: {}, 264 | PackageType: 'Zip' 265 | } 266 | 267 | if (this._isUseS3(program)) { 268 | params.Code = { 269 | S3Bucket: null, 270 | S3Key: null 271 | } 272 | } else if (this._useECR(program)) { 273 | params.Code = { ImageUri: program.imageUri } 274 | params.PackageType = 'Image' 275 | delete params.Handler 276 | delete params.Runtime 277 | delete params.KMSKeyArn 278 | } else { 279 | params.Code = { ZipFile: buffer } 280 | } 281 | 282 | // Escape characters that is not allowed by AWS Lambda 283 | params.FunctionName = params.FunctionName.replace(/[^a-zA-Z0-9-_]/g, '_') 284 | 285 | if (program.vpcSubnets && program.vpcSecurityGroups) { 286 | params.VpcConfig = { 287 | SubnetIds: program.vpcSubnets.split(','), 288 | SecurityGroupIds: program.vpcSecurityGroups.split(',') 289 | } 290 | } 291 | if (program.configFile) { 292 | const configValues = fs.readFileSync(program.configFile) 293 | const config = dotenv.parse(configValues) 294 | // If `configFile` is an empty file, `config` value will be `{}` 295 | params.Environment = { 296 | Variables: config 297 | } 298 | } 299 | if (program.deadLetterConfigTargetArn !== undefined) { 300 | params.DeadLetterConfig = { 301 | TargetArn: program.deadLetterConfigTargetArn 302 | } 303 | } 304 | if (program.tracingConfig) { 305 | params.TracingConfig.Mode = program.tracingConfig 306 | } 307 | if (program.layers) { 308 | params.Layers = program.layers.split(',') 309 | } 310 | if (program.tags) { 311 | const tags = program.tags.split(',') 312 | for (const tag of tags) { 313 | const kvPair = tag.split('=') 314 | if (kvPair && kvPair.length === 2) { 315 | params.Tags[kvPair[0].toString()] = kvPair[1].toString() 316 | } 317 | } 318 | } 319 | 320 | return params 321 | } 322 | 323 | _eventSourceList (program) { 324 | if (!program.eventSourceFile) { 325 | return { 326 | EventSourceMappings: null, 327 | ScheduleEvents: null, 328 | S3Events: null 329 | } 330 | } 331 | const list = fs.readJsonSync(program.eventSourceFile) 332 | 333 | if (Array.isArray(list)) { 334 | // backward-compatible 335 | return { 336 | EventSourceMappings: list, 337 | ScheduleEvents: [], 338 | S3Events: [] 339 | } 340 | } 341 | if (!list.EventSourceMappings) { 342 | list.EventSourceMappings = [] 343 | } 344 | if (!list.ScheduleEvents) { 345 | list.ScheduleEvents = [] 346 | } 347 | if (!list.S3Events) { 348 | list.S3Events = [] 349 | } 350 | return list 351 | } 352 | 353 | _fileCopy (program, src, dest, excludeNodeModules) { 354 | const excludes = (() => { 355 | return [ 356 | '.git*', 357 | '*.swp', 358 | '.editorconfig', 359 | '.lambda', 360 | 'deploy.env', 361 | '*.log' 362 | ] 363 | .concat(program.excludeGlobs ? program.excludeGlobs.split(' ') : []) 364 | .concat(excludeNodeModules ? [path.join('node_modules')] : []) 365 | })() 366 | 367 | // Formatting for `filter` of `fs.copy` 368 | const dirBlobs = [] 369 | const pattern = '{' + excludes.map((str) => { 370 | if (str.charAt(str.length - 1) === path.sep) { 371 | str = str.substr(0, str.length - 1) 372 | dirBlobs.push(str) 373 | } 374 | return str 375 | }).join(',') + '}' 376 | const dirPatternRegExp = dirBlobs.length > 0 ? new RegExp(`(${dirBlobs.join('|')})$`) : null 377 | 378 | return new Promise((resolve, reject) => { 379 | fs.mkdirs(dest, (err) => { 380 | if (err) return reject(err) 381 | const options = { 382 | dereference: true, // same meaning as `-L` of `rsync` command 383 | filter: (src, dest) => { 384 | if (!program.prebuiltDirectory && ['package.json', 'package-lock.json'].includes(src)) { 385 | // include package.json & package-lock.json unless prebuiltDirectory is set 386 | return true 387 | } 388 | 389 | if (!minimatch(src, pattern, { matchBase: true, windowsPathsNoEscape: true })) { 390 | return true 391 | } 392 | 393 | // Directory check. Even if `src` is a directory it will not end with '/'. 394 | if (dirPatternRegExp === null || !dirPatternRegExp.test(src)) { 395 | return false 396 | } 397 | 398 | return !fs.statSync(src).isDirectory() 399 | } 400 | } 401 | fs.copy(src, dest, options, (err) => { 402 | if (err) return reject(err) 403 | resolve() 404 | }) 405 | }) 406 | }) 407 | } 408 | 409 | _shouldUseNpmCi (codeDirectory) { 410 | return fs.existsSync(path.join(codeDirectory, 'package-lock.json')) 411 | } 412 | 413 | _getNpmInstallCommand (program, codeDirectory) { 414 | const installOptions = [ 415 | '-s', 416 | this._shouldUseNpmCi(codeDirectory) ? 'ci' : 'install', 417 | '--production', 418 | '--no-audit' 419 | ] 420 | 421 | if (program.optionalDependencies === false) { 422 | installOptions.push('--no-optional') 423 | } 424 | 425 | if (!program.dockerImage) { 426 | installOptions.push('--prefix', codeDirectory) 427 | } 428 | 429 | return { 430 | packageManager: 'npm', 431 | installOptions 432 | } 433 | } 434 | 435 | _getYarnInstallCommand (program, codeDirectory) { 436 | const installOptions = [ 437 | '-s', 438 | 'install', 439 | '--production' 440 | ] 441 | 442 | if (program.optionalDependencies === false) { 443 | installOptions.push('--ignore-optional') 444 | } 445 | 446 | if (!program.dockerImage) { 447 | installOptions.push('--cwd', codeDirectory) 448 | } 449 | 450 | return { 451 | packageManager: 'yarn', 452 | installOptions 453 | } 454 | } 455 | 456 | _packageInstall (program, codeDirectory) { 457 | if (!fs.existsSync(path.join(codeDirectory, 'package.json'))) { 458 | console.log('Skip the installation of the package. (Because package.json is not found.)') 459 | return 460 | } 461 | 462 | // Run on windows: 463 | // https://nodejs.org/api/child_process.html#child_process_spawning_bat_and_cmd_files_on_windows 464 | 465 | const { packageManager, installOptions } = (() => { 466 | // default npm 467 | if (program.packageManager === 'yarn') { 468 | return this._getYarnInstallCommand(program, codeDirectory) 469 | } 470 | return this._getNpmInstallCommand(program, codeDirectory) 471 | })() 472 | 473 | const paramsOnContainer = (() => { 474 | // with docker 475 | let dockerVolumesOptions = [] 476 | program.dockerVolumes && program.dockerVolumes.split(' ').forEach((volume) => { 477 | dockerVolumesOptions = dockerVolumesOptions.concat(['-v', volume]) 478 | }) 479 | const dockerCommand = [program.dockerImage, packageManager].concat(installOptions) 480 | const dockerBaseOptions = [ 481 | 'run', '--rm', 482 | '-v', `${fs.realpathSync(codeDirectory)}:/var/task`, 483 | '-w', '/var/task' 484 | ] 485 | const dockerOptions = dockerBaseOptions.concat(dockerVolumesOptions).concat(dockerCommand) 486 | if (process.platform === 'win32') { 487 | return { 488 | command: 'cmd.exe', 489 | options: ['/c', 'docker'].concat(dockerOptions) 490 | } 491 | } 492 | return { 493 | command: 'docker', 494 | options: dockerOptions 495 | } 496 | })() 497 | 498 | const paramsOnHost = (() => { 499 | // simple install 500 | if (process.platform === 'win32') { 501 | return { 502 | command: 'cmd.exe', 503 | options: ['/c', packageManager].concat(installOptions) 504 | } 505 | } 506 | return { 507 | command: packageManager, 508 | options: installOptions 509 | } 510 | })() 511 | 512 | const params = program.dockerImage ? paramsOnContainer : paramsOnHost 513 | return new Promise((resolve, reject) => { 514 | execFile(params.command, params.options, { 515 | maxBuffer: maxBufferSize, 516 | env: process.env 517 | }, (err) => { 518 | if (err) return reject(err) 519 | resolve(packageManager) 520 | }) 521 | }) 522 | } 523 | 524 | _postInstallScript (program, codeDirectory) { 525 | const scriptFilename = 'post_install.sh' 526 | const filePath = path.join(codeDirectory, scriptFilename) 527 | if (!fs.existsSync(filePath)) return Promise.resolve() 528 | 529 | const cmd = path.join(codeDirectory, scriptFilename) + ' ' + program.environment 530 | console.log('=> Running post install script ' + scriptFilename) 531 | 532 | return new Promise((resolve, reject) => { 533 | exec(cmd, { 534 | env: process.env, 535 | cwd: codeDirectory, 536 | maxBuffer: maxBufferSize 537 | }, (error, stdout, stderr) => { 538 | if (error) { 539 | return reject(new Error(`${error} stdout: ${stdout} stderr: ${stderr}`)) 540 | } 541 | console.log('\t\t' + stdout) 542 | resolve() 543 | }) 544 | }) 545 | } 546 | 547 | _zip (program, codeDirectory) { 548 | console.log('=> Zipping repo. This might take up to 30 seconds') 549 | 550 | const tmpZipFile = path.join(os.tmpdir(), +(new Date()) + '.zip') 551 | const output = fs.createWriteStream(tmpZipFile) 552 | const archive = archiver('zip', { 553 | zlib: { level: 9 } // Sets the compression level. 554 | }) 555 | return new Promise((resolve) => { 556 | output.on('close', () => { 557 | const contents = fs.readFileSync(tmpZipFile) 558 | fs.unlinkSync(tmpZipFile) 559 | resolve(contents) 560 | }) 561 | archive.pipe(output) 562 | klaw(codeDirectory, { preserveSymlinks: true }) 563 | .on('data', (file) => { 564 | if (file.stats.isDirectory()) return 565 | 566 | const filePath = file.path.replace(path.join(codeDirectory, path.sep), '') 567 | if (file.stats.isSymbolicLink()) { 568 | return archive.symlink(filePath, fs.readlinkSync(file.path)) 569 | } 570 | 571 | archive.append( 572 | fs.createReadStream(file.path), 573 | { 574 | name: filePath, 575 | stats: file.stats 576 | } 577 | ) 578 | }) 579 | .on('end', () => { 580 | archive.finalize() 581 | }) 582 | }) 583 | } 584 | 585 | _codeDirectory () { 586 | // Why realpathSync?: 587 | // If tmpdir is symbolic link and npm>=7, `this._packageInstall()` may not work properly. 588 | return path.join(fs.realpathSync(os.tmpdir()), `${path.basename(path.resolve('.'))}-lambda`) 589 | } 590 | 591 | _cleanDirectory (codeDirectory, keepNodeModules) { 592 | if (!fs.existsSync(codeDirectory)) { 593 | return new Promise((resolve, reject) => { 594 | fs.mkdirs(codeDirectory, (err) => { 595 | if (err) return reject(err) 596 | resolve() 597 | }) 598 | }) 599 | } 600 | return new Promise((resolve, reject) => { 601 | fs.readdir(codeDirectory, (err, files) => { 602 | if (err) return reject(err) 603 | 604 | Promise.all(files.map(file => { 605 | return new Promise((resolve, reject) => { 606 | if (keepNodeModules && file === 'node_modules') { 607 | resolve() 608 | } else { 609 | fs.remove(path.join(codeDirectory, file), err => { 610 | if (err) return reject(err) 611 | resolve() 612 | }) 613 | } 614 | }) 615 | })).then(() => { 616 | resolve() 617 | }) 618 | }) 619 | }) 620 | } 621 | 622 | _setRunTimeEnvironmentVars (program) { 623 | const configValues = fs.readFileSync(program.configFile) 624 | const config = dotenv.parse(configValues) 625 | 626 | for (const k in config) { 627 | if (!Object.getOwnPropertyDescriptor(config, k)) { 628 | continue 629 | } 630 | 631 | process.env[k] = config[k] 632 | } 633 | } 634 | 635 | async _uploadExisting (lambda, params) { 636 | const functionCodeParams = Object.assign({ 637 | FunctionName: params.FunctionName, 638 | Publish: params.Publish, 639 | Architectures: params.Architectures 640 | }, params.Code) 641 | 642 | const functionConfigParams = { 643 | FunctionName: params.FunctionName, 644 | Description: params.Description, 645 | Handler: params.Handler, 646 | MemorySize: params.MemorySize, 647 | Role: params.Role, 648 | Timeout: params.Timeout, 649 | Runtime: params.Runtime, 650 | VpcConfig: params.VpcConfig, 651 | Environment: params.Environment, 652 | KMSKeyArn: params.KMSKeyArn, 653 | DeadLetterConfig: params.DeadLetterConfig, 654 | TracingConfig: params.TracingConfig, 655 | Layers: params.Layers 656 | } 657 | if (functionCodeParams.ImageUri != null) { 658 | delete functionConfigParams.Handler 659 | delete functionConfigParams.Runtime 660 | delete functionConfigParams.KMSKeyArn 661 | delete functionConfigParams.Layers 662 | } 663 | 664 | const updateConfigRequest = lambda.updateFunctionConfiguration(functionConfigParams) 665 | updateConfigRequest.on('retry', (response) => { 666 | console.log(response.error.message) 667 | console.log('=> Retrying') 668 | }) 669 | const updateConfigResponse = await updateConfigRequest.promise() 670 | 671 | // Wait for the `Configuration.LastUpdateStatus` to change from `InProgress` to `Successful`. 672 | for (let i = 0; i < 10; i++) { 673 | const data = await lambda.getFunction({ FunctionName: params.FunctionName }).promise() 674 | if (data.Configuration.LastUpdateStatus === 'Successful') { 675 | break 676 | } 677 | await new Promise((resolve) => setTimeout(resolve, 3000)) 678 | } 679 | 680 | const updateCodeRequest = lambda.updateFunctionCode(functionCodeParams) 681 | updateCodeRequest.on('retry', (response) => { 682 | console.log(response.error.message) 683 | console.log('=> Retrying') 684 | }) 685 | await updateCodeRequest.promise() 686 | 687 | return updateConfigResponse 688 | } 689 | 690 | _uploadNew (lambda, params) { 691 | return new Promise((resolve, reject) => { 692 | const request = lambda.createFunction(params, (err, data) => { 693 | if (err) return reject(err) 694 | resolve(data) 695 | }) 696 | request.on('retry', (response) => { 697 | console.log(response.error.message) 698 | console.log('=> Retrying') 699 | }) 700 | }) 701 | } 702 | 703 | _readArchive (program) { 704 | if (!fs.existsSync(program.deployZipfile)) { 705 | const err = new Error('No such Zipfile [' + program.deployZipfile + ']') 706 | return Promise.reject(err) 707 | } 708 | return new Promise((resolve, reject) => { 709 | fs.readFile(program.deployZipfile, (err, data) => { 710 | if (err) return reject(err) 711 | resolve(data) 712 | }) 713 | }) 714 | } 715 | 716 | _archive (program) { 717 | if (program.deployZipfile && fs.existsSync(program.deployZipfile)) { 718 | return this._readArchive(program) 719 | } 720 | return program.prebuiltDirectory 721 | ? this._archivePrebuilt(program) 722 | : this._buildAndArchive(program) 723 | } 724 | 725 | _archivePrebuilt (program) { 726 | const codeDirectory = this._codeDirectory() 727 | 728 | return Promise.resolve().then(() => { 729 | return this._cleanDirectory(codeDirectory, program.keepNodeModules) 730 | }).then(() => { 731 | return this._fileCopy(program, program.prebuiltDirectory, codeDirectory, false).then(() => { 732 | console.log('=> Zipping deployment package') 733 | return this._zip(program, codeDirectory) 734 | }) 735 | }) 736 | } 737 | 738 | async _buildAndArchive (program) { 739 | if (!fs.existsSync('.env')) { 740 | console.warn('[Warning] `.env` file does not exist.') 741 | console.info('Execute `node-lambda setup` as necessary and set it up.') 742 | } 743 | 744 | // Warn if not building on 64-bit linux 745 | const arch = process.platform + '.' + process.arch 746 | if (arch !== 'linux.x64' && !program.dockerImage) { 747 | console.warn(`Warning!!! You are building on a platform that is not 64-bit Linux (${arch}). 748 | If any of your Node dependencies include C-extensions, \ 749 | they may not work as expected in the Lambda environment. 750 | 751 | `) 752 | } 753 | 754 | const codeDirectory = this._codeDirectory() 755 | const lambdaSrcDirectory = program.sourceDirectory ? program.sourceDirectory.replace(/\/$/, '') : '.' 756 | 757 | await this._cleanDirectory(codeDirectory, program.keepNodeModules) 758 | console.log('=> Moving files to temporary directory') 759 | await this._fileCopy(program, lambdaSrcDirectory, codeDirectory, true) 760 | if (!program.keepNodeModules) { 761 | console.log('=> Running package install') 762 | const usedPackageManager = await this._packageInstall(program, codeDirectory) 763 | if (usedPackageManager) { 764 | console.log(`(Package manager used was '${usedPackageManager}'.)`) 765 | } 766 | } 767 | await this._postInstallScript(program, codeDirectory) 768 | console.log('=> Zipping deployment package') 769 | return this._zip(program, codeDirectory) 770 | } 771 | 772 | _listEventSourceMappings (lambda, params) { 773 | return new Promise((resolve, reject) => { 774 | lambda.listEventSourceMappings(params, (err, data) => { 775 | if (err) return reject(err) 776 | if (data && data.EventSourceMappings) { 777 | return resolve(data.EventSourceMappings) 778 | } 779 | return resolve([]) 780 | }) 781 | }) 782 | } 783 | 784 | _getStartingPosition (eventSource) { 785 | if (eventSource.EventSourceArn.startsWith('arn:aws:sqs:')) { 786 | return null 787 | } 788 | return eventSource.StartingPosition ? eventSource.StartingPosition : 'LATEST' 789 | } 790 | 791 | _updateEventSources (lambda, functionName, existingEventSourceList, eventSourceList) { 792 | if (eventSourceList == null) { 793 | return Promise.resolve([]) 794 | } 795 | const updateEventSourceList = [] 796 | // Checking new and update event sources 797 | for (const i in eventSourceList) { 798 | let isExisting = false 799 | for (const j in existingEventSourceList) { 800 | if (eventSourceList[i].EventSourceArn === existingEventSourceList[j].EventSourceArn) { 801 | isExisting = true 802 | updateEventSourceList.push({ 803 | type: 'update', 804 | FunctionName: functionName, 805 | Enabled: eventSourceList[i].Enabled, 806 | BatchSize: eventSourceList[i].BatchSize, 807 | UUID: existingEventSourceList[j].UUID 808 | }) 809 | break 810 | } 811 | } 812 | 813 | // If it is new source 814 | if (!isExisting) { 815 | updateEventSourceList.push({ 816 | type: 'create', 817 | FunctionName: functionName, 818 | EventSourceArn: eventSourceList[i].EventSourceArn, 819 | Enabled: eventSourceList[i].Enabled ? eventSourceList[i].Enabled : false, 820 | BatchSize: eventSourceList[i].BatchSize ? eventSourceList[i].BatchSize : 100, 821 | StartingPosition: this._getStartingPosition(eventSourceList[i]) 822 | }) 823 | } 824 | } 825 | 826 | // Checking delete event sources 827 | for (const i in existingEventSourceList) { 828 | let isExisting = false 829 | for (const j in eventSourceList) { 830 | if (eventSourceList[j].EventSourceArn === existingEventSourceList[i].EventSourceArn) { 831 | isExisting = true 832 | break 833 | } 834 | } 835 | 836 | // If delete the source 837 | if (!isExisting) { 838 | updateEventSourceList.push({ 839 | type: 'delete', 840 | UUID: existingEventSourceList[i].UUID 841 | }) 842 | } 843 | } 844 | 845 | return Promise.all(updateEventSourceList.map((updateEventSource) => { 846 | switch (updateEventSource.type) { 847 | case 'create': 848 | delete updateEventSource.type 849 | return new Promise((resolve, reject) => { 850 | lambda.createEventSourceMapping(updateEventSource, (err, data) => { 851 | if (err) return reject(err) 852 | resolve(data) 853 | }) 854 | }) 855 | case 'update': 856 | delete updateEventSource.type 857 | return new Promise((resolve, reject) => { 858 | lambda.updateEventSourceMapping(updateEventSource, (err, data) => { 859 | if (err) return reject(err) 860 | resolve(data) 861 | }) 862 | }) 863 | case 'delete': 864 | delete updateEventSource.type 865 | return new Promise((resolve, reject) => { 866 | lambda.deleteEventSourceMapping(updateEventSource, (err, data) => { 867 | if (err) return reject(err) 868 | resolve(data) 869 | }) 870 | }) 871 | } 872 | return Promise.resolve() 873 | })) 874 | } 875 | 876 | _updateTags (lambda, functionArn, tags) { 877 | if (!tags || Object.keys(tags).length <= 0) { 878 | return Promise.resolve([]) 879 | } else { 880 | return lambda.listTags({ Resource: functionArn }).promise() 881 | .then(data => { 882 | const keys = Object.keys(data.Tags) 883 | return keys && keys.length > 0 884 | ? lambda.untagResource({ Resource: functionArn, TagKeys: keys }).promise() 885 | : Promise.resolve() 886 | }) 887 | .then(() => { 888 | return lambda.tagResource({ Resource: functionArn, Tags: tags }).promise() 889 | }) 890 | } 891 | } 892 | 893 | _updateScheduleEvents (scheduleEvents, functionArn, scheduleList) { 894 | if (scheduleList == null) { 895 | return Promise.resolve([]) 896 | } 897 | 898 | const paramsList = scheduleList.map((schedule) => 899 | Object.assign(schedule, { FunctionArn: functionArn })) 900 | 901 | // series 902 | return paramsList.map((params) => { 903 | return scheduleEvents.add(params) 904 | }).reduce((a, b) => { 905 | return a.then(b) 906 | }, Promise.resolve()).then(() => { 907 | // Since `scheduleEvents.add(params)` returns only `{}` if it succeeds 908 | // it is not very meaningful. 909 | // Therefore, return the params used for execution 910 | return paramsList 911 | }) 912 | } 913 | 914 | _updateS3Events (s3Events, functionArn, s3EventsList) { 915 | if (s3EventsList == null) return Promise.resolve([]) 916 | 917 | const paramsList = s3EventsList.map(s3event => 918 | Object.assign(s3event, { FunctionArn: functionArn })) 919 | 920 | return s3Events.add(paramsList).then(() => { 921 | // Since it is similar to _updateScheduleEvents, it returns meaningful values 922 | return paramsList 923 | }) 924 | } 925 | 926 | _setLogsRetentionPolicy (cloudWatchLogs, program, functionName) { 927 | const days = parseInt(program.retentionInDays) 928 | if (!Number.isInteger(days)) return Promise.resolve({}) 929 | return cloudWatchLogs.setLogsRetentionPolicy({ 930 | FunctionName: functionName, 931 | retentionInDays: days 932 | }).then(() => { 933 | // Since it is similar to _updateScheduleEvents, it returns meaningful values 934 | return { retentionInDays: days } 935 | }) 936 | } 937 | 938 | package (program) { 939 | if (!program.packageDirectory) { 940 | throw new Error('packageDirectory not specified!') 941 | } 942 | try { 943 | const isDir = fs.lstatSync(program.packageDirectory).isDirectory() 944 | 945 | if (!isDir) { 946 | throw new Error(program.packageDirectory + ' is not a directory!') 947 | } 948 | } catch (err) { 949 | if (err.code !== 'ENOENT') { 950 | throw err 951 | } 952 | console.log('=> Creating package directory') 953 | fs.mkdirsSync(program.packageDirectory) 954 | } 955 | 956 | return this._archive(program).then((buffer) => { 957 | const basename = program.functionName + (program.environment ? '-' + program.environment : '') 958 | const zipfile = path.join(program.packageDirectory, basename + '.zip') 959 | console.log('=> Writing packaged zip') 960 | fs.writeFile(zipfile, buffer, (err) => { 961 | if (err) { 962 | throw err 963 | } 964 | console.log('Packaged zip created: ' + zipfile) 965 | }) 966 | }).catch((err) => { 967 | throw err 968 | }) 969 | } 970 | 971 | _isFunctionDoesNotExist (err) { 972 | return err.code === 'ResourceNotFoundException' && 973 | !!err.message.match(/^Function not found:/) 974 | } 975 | 976 | async _deployToRegion (program, params, region, buffer) { 977 | aws.updateConfig(program, region) 978 | 979 | console.log('=> Reading event source file to memory') 980 | const eventSourceList = this._eventSourceList(program) 981 | 982 | if (this._isUseS3(program)) { 983 | const s3Deploy = new S3Deploy(aws.sdk, region) 984 | params.Code = await s3Deploy.putPackage(params, region, buffer) 985 | console.log(`=> Uploading AWS Lambda ${region} with parameters:`) 986 | } else { 987 | console.log(`=> Uploading zip file to AWS Lambda ${region} with parameters:`) 988 | } 989 | console.log(params) 990 | const lambda = new aws.sdk.Lambda({ 991 | region, 992 | apiVersion: '2015-03-31' 993 | }) 994 | const scheduleEvents = new ScheduleEvents(aws.sdk, region) 995 | const s3Events = new S3Events(aws.sdk, region) 996 | const cloudWatchLogs = new CloudWatchLogs(aws.sdk, region) 997 | 998 | const existsFunction = await (async () => { 999 | try { 1000 | await lambda.getFunction({ 1001 | FunctionName: params.FunctionName 1002 | }).promise() 1003 | return true 1004 | } catch (err) { 1005 | if (!this._isFunctionDoesNotExist(err)) { 1006 | throw err 1007 | } 1008 | return false 1009 | } 1010 | })() 1011 | 1012 | if (existsFunction) { 1013 | const existingEventSourceList = await this._listEventSourceMappings(lambda, { 1014 | FunctionName: params.FunctionName 1015 | }) 1016 | const results = await this._uploadExisting(lambda, params) 1017 | console.log('=> Done uploading. Results follow: ') 1018 | console.log(results) 1019 | 1020 | return Promise.all([ 1021 | Promise.all([ 1022 | this._updateScheduleEvents( 1023 | scheduleEvents, 1024 | results.FunctionArn, 1025 | eventSourceList.ScheduleEvents 1026 | ), 1027 | this._updateS3Events( 1028 | s3Events, 1029 | results.FunctionArn, 1030 | eventSourceList.S3Events 1031 | ), 1032 | this._updateTags( 1033 | lambda, 1034 | results.FunctionArn, 1035 | params.Tags) 1036 | ]), 1037 | this._updateEventSources( 1038 | lambda, 1039 | params.FunctionName, 1040 | existingEventSourceList, 1041 | eventSourceList.EventSourceMappings 1042 | ), 1043 | this._setLogsRetentionPolicy( 1044 | cloudWatchLogs, 1045 | program, 1046 | params.FunctionName 1047 | ) 1048 | ]) 1049 | } else { 1050 | const results = await this._uploadNew(lambda, params) 1051 | console.log('=> Done uploading. Results follow: ') 1052 | console.log(results) 1053 | 1054 | return Promise.all([ 1055 | this._updateEventSources( 1056 | lambda, 1057 | params.FunctionName, 1058 | [], 1059 | eventSourceList.EventSourceMappings 1060 | ), 1061 | this._updateScheduleEvents( 1062 | scheduleEvents, 1063 | results.FunctionArn, 1064 | eventSourceList.ScheduleEvents 1065 | ), 1066 | this._updateS3Events( 1067 | s3Events, 1068 | results.FunctionArn, 1069 | eventSourceList.S3Events 1070 | ), 1071 | this._setLogsRetentionPolicy( 1072 | cloudWatchLogs, 1073 | program, 1074 | params.FunctionName 1075 | ) 1076 | ]) 1077 | } 1078 | } 1079 | 1080 | _printDeployResults (results, isFirst) { 1081 | if (!Array.isArray(results)) { 1082 | if (results == null) return 1083 | console.log(results) 1084 | return 1085 | } 1086 | if (results.length === 0) return 1087 | 1088 | if (isFirst === true) console.log('=> All tasks done. Results follow:') 1089 | results.forEach(result => { 1090 | this._printDeployResults(result) 1091 | }) 1092 | } 1093 | 1094 | async deploy (program) { 1095 | const regions = program.region.split(',') 1096 | let buffer = null 1097 | if (!this._useECR(program)) { 1098 | try { 1099 | buffer = await this._archive(program) 1100 | console.log('=> Reading zip file to memory') 1101 | } catch (err) { 1102 | process.exitCode = 1 1103 | console.log(err) 1104 | return 1105 | } 1106 | } 1107 | 1108 | try { 1109 | const params = this._params(program, buffer) 1110 | const results = await Promise.all(regions.map((region) => { 1111 | return this._deployToRegion( 1112 | program, 1113 | params, 1114 | region, 1115 | this._isUseS3(program) ? buffer : null 1116 | ) 1117 | })) 1118 | this._printDeployResults(results, true) 1119 | } catch (err) { 1120 | process.exitCode = 1 1121 | console.log(err) 1122 | } 1123 | } 1124 | } 1125 | 1126 | module.exports = new Lambda() 1127 | -------------------------------------------------------------------------------- /lib/s3_deploy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const process = require('process') 4 | const crypto = require('crypto') 5 | // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createBucket-property 6 | const S3_LOCATION_POSSIBLE_VALUES = [ 7 | 'EU', 8 | 'af-south-1', 9 | 'ap-east-1', 10 | 'ap-northeast-1', 11 | 'ap-northeast-2', 12 | 'ap-northeast-3', 13 | 'ap-south-1', 14 | 'ap-southeast-1', 15 | 'ap-southeast-2', 16 | 'ap-southeast-3', 17 | 'ca-central-1', 18 | 'cn-north-1', 19 | 'cn-northwest-1', 20 | 'eu-central-1', 21 | 'eu-north-1', 22 | 'eu-south-1', 23 | 'eu-west-1', 24 | 'eu-west-2', 25 | 'eu-west-3', 26 | 'me-south-1', 27 | 'sa-east-1', 28 | 'us-east-2', 29 | 'us-gov-east-1', 30 | 'us-gov-west-1', 31 | 'us-west-1', 32 | 'us-west-2' 33 | ] 34 | 35 | class S3Deploy { 36 | constructor (aws, region) { 37 | // Authenticated `aws` object in `lib/main.js` 38 | this.s3 = new aws.S3({ 39 | region, 40 | apiVersion: '2006-03-01' 41 | }) 42 | } 43 | 44 | _md5 (str) { 45 | return crypto 46 | .createHash('md5') 47 | .update(str, 'utf8') 48 | .digest('hex') 49 | } 50 | 51 | _convertRegionStringToEnvVarName (region) { 52 | if (region == null) return 'undefined' 53 | return region.replace(/-/g, '_').toUpperCase() 54 | } 55 | 56 | _getBucketNameFromEnvVar (region) { 57 | const key = [ 58 | 'S3', 59 | this._convertRegionStringToEnvVarName(region), 60 | 'BUCKET' 61 | ].join('_') 62 | return process.env[key] 63 | } 64 | 65 | _getS3KeyPrefixFromEnvVar (region) { 66 | const key = [ 67 | 'S3', 68 | this._convertRegionStringToEnvVarName(region), 69 | 'PREFIX' 70 | ].join('_') 71 | return process.env[key] 72 | } 73 | 74 | _bucketName (params) { 75 | const bucketNameFromEnvVar = this._getBucketNameFromEnvVar(params.region) 76 | if (bucketNameFromEnvVar != null) return bucketNameFromEnvVar 77 | 78 | return [ 79 | params.FunctionName, 80 | params.region, 81 | this._md5(params.FunctionName + params.region) 82 | ] 83 | .join('-') 84 | .substr(0, 63).toLowerCase() 85 | } 86 | 87 | _s3Key (params) { 88 | const s3Prefix = this._getS3KeyPrefixFromEnvVar(params.region) 89 | const keys = [`deploy-package-${params.FunctionName}.zip`] 90 | if (s3Prefix != null) { 91 | keys.unshift(s3Prefix.replace(/\/$/, '')) 92 | } 93 | return keys.join('/') 94 | } 95 | 96 | _getS3Location (region) { 97 | return S3_LOCATION_POSSIBLE_VALUES.includes(region) ? region : null 98 | } 99 | 100 | _createBucket (params) { 101 | const _params = { 102 | Bucket: params.bucketName 103 | } 104 | const s3Location = this._getS3Location(params.region) 105 | if (s3Location != null) { 106 | _params.CreateBucketConfiguration = { 107 | LocationConstraint: s3Location 108 | } 109 | } 110 | return new Promise((resolve, reject) => { 111 | this.s3.createBucket(_params, (err, data) => { 112 | if (err) { 113 | // Ignored created 114 | if (err.code === 'BucketAlreadyOwnedByYou') return resolve({}) 115 | return reject(err) 116 | } 117 | resolve(data) 118 | }) 119 | }) 120 | } 121 | 122 | _putObject (params, buffer) { 123 | const _params = { 124 | Body: buffer, 125 | Bucket: params.bucketName, 126 | Key: params.s3Key 127 | } 128 | return new Promise((resolve, reject) => { 129 | this.s3.putObject(_params, (err, data) => { 130 | if (err) reject(err) 131 | resolve(data) 132 | }) 133 | }) 134 | } 135 | 136 | putPackage (params, region, buffer) { 137 | const _params = Object.assign({ region }, params) 138 | _params.bucketName = this._bucketName(_params) 139 | _params.s3Key = this._s3Key(_params) 140 | 141 | return this._createBucket(_params).then((result) => { 142 | if (result.Location != null) { 143 | console.log('=> S3 Bucket created:') 144 | console.log(`===> ${_params.bucketName}`) 145 | } 146 | return this._putObject(_params, buffer) 147 | }).then((result) => { 148 | console.log('=> Deploy the zip file to S3:') 149 | console.log(`===> ${_params.bucketName}/${_params.s3Key}`) 150 | return { 151 | S3Bucket: _params.bucketName, 152 | S3Key: _params.s3Key 153 | } 154 | }) 155 | } 156 | } 157 | 158 | module.exports = S3Deploy 159 | -------------------------------------------------------------------------------- /lib/s3_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Do not create S3 bucket. 5 | * Put the Notification Configuration in the existing Bucket. 6 | */ 7 | class S3Events { 8 | constructor (aws, region) { 9 | // Authenticated `aws` object in `lib/main.js` 10 | this.lambda = new aws.Lambda({ 11 | region, 12 | apiVersion: '2015-03-31' 13 | }) 14 | this.s3 = new aws.S3({ 15 | region, 16 | apiVersion: '2006-03-01' 17 | }) 18 | } 19 | 20 | _functionName (params) { 21 | return params.FunctionArn.split(':').pop() 22 | } 23 | 24 | _statementId (params) { 25 | return params.Bucket.replace(/[^a-zA-Z0-9-_]/g, '_') 26 | } 27 | 28 | _addPermissionParams (params) { 29 | return { 30 | Action: 'lambda:InvokeFunction', 31 | FunctionName: this._functionName(params), 32 | Principal: 's3.amazonaws.com', 33 | SourceArn: 'arn:aws:s3:::' + params.Bucket, 34 | StatementId: this._statementId(params) 35 | } 36 | } 37 | 38 | _addPermission (params) { 39 | return new Promise((resolve, reject) => { 40 | const _params = this._addPermissionParams(params) 41 | this.lambda.addPermission(_params, (err, data) => { 42 | if (err) { 43 | if (err.code !== 'ResourceConflictException') reject(err) 44 | // If it exists it will result in an error but there is no problem. 45 | resolve('Permission already set') 46 | } 47 | resolve(data) 48 | }) 49 | }) 50 | } 51 | 52 | _lambdaFunctionConfiguration (params) { 53 | const lambdaFunctionConfiguration = { 54 | Events: params.Events, 55 | LambdaFunctionArn: params.FunctionArn 56 | } 57 | if (params.Filter != null) { 58 | lambdaFunctionConfiguration.Filter = params.Filter 59 | } 60 | 61 | return lambdaFunctionConfiguration 62 | } 63 | 64 | _paramsListToBucketNotificationConfigurations (paramsList) { 65 | const lambdaFunctionConfigurations = {} 66 | for (const params of paramsList) { 67 | if (lambdaFunctionConfigurations[params.Bucket] == null) { 68 | lambdaFunctionConfigurations[params.Bucket] = [ 69 | this._lambdaFunctionConfiguration(params) 70 | ] 71 | continue 72 | } 73 | lambdaFunctionConfigurations[params.Bucket].push( 74 | this._lambdaFunctionConfiguration(params) 75 | ) 76 | } 77 | return Object.keys(lambdaFunctionConfigurations).map((bucket) => { 78 | return { 79 | Bucket: bucket, 80 | NotificationConfiguration: { 81 | LambdaFunctionConfigurations: 82 | lambdaFunctionConfigurations[bucket] 83 | } 84 | } 85 | }) 86 | } 87 | 88 | _putBucketNotificationConfiguration (putBucketNotificationConfigurationParams) { 89 | return new Promise((resolve, reject) => { 90 | this.s3.putBucketNotificationConfiguration(putBucketNotificationConfigurationParams, (err, data) => { 91 | if (err) reject(err) 92 | resolve(data) 93 | }) 94 | }) 95 | } 96 | 97 | add (paramsList) { 98 | return paramsList.map(params => { 99 | return this._addPermission(params) 100 | }).reduce((a, b) => { 101 | return a.then(b) 102 | }, Promise.resolve()).then(() => { 103 | return this._paramsListToBucketNotificationConfigurations(paramsList).map(putBucketNotificationConfigurationParams => { 104 | return this._putBucketNotificationConfiguration(putBucketNotificationConfigurationParams) 105 | }).reduce((a, b) => { 106 | return a.then(b) 107 | }, Promise.resolve({})) 108 | }) 109 | } 110 | } 111 | 112 | module.exports = S3Events 113 | -------------------------------------------------------------------------------- /lib/schedule_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ScheduleEvents { 4 | constructor (aws, region) { 5 | // Authenticated `aws` object in `lib/main.js` 6 | this.lambda = new aws.Lambda({ 7 | region, 8 | apiVersion: '2015-03-31' 9 | }) 10 | this.cloudwatchevents = new aws.CloudWatchEvents({ 11 | apiVersion: '2015-10-07' 12 | }) 13 | } 14 | 15 | _ruleDescription (params) { 16 | if ('ScheduleDescription' in params && params.ScheduleDescription != null) { 17 | return `${params.ScheduleDescription}` 18 | } 19 | return `${params.ScheduleName} - ${params.ScheduleExpression}` 20 | } 21 | 22 | _functionName (params) { 23 | return params.FunctionArn.split(':').pop() 24 | } 25 | 26 | _putRulePrams (params) { 27 | return { 28 | Name: params.ScheduleName, 29 | Description: this._ruleDescription(params), 30 | State: params.ScheduleState, 31 | ScheduleExpression: params.ScheduleExpression 32 | } 33 | } 34 | 35 | _putRule (params) { 36 | // return RuleArn if created 37 | return new Promise((resolve, reject) => { 38 | const _params = this._putRulePrams(params) 39 | this.cloudwatchevents.putRule(_params, (err, rule) => { 40 | if (err) reject(err) 41 | resolve(rule) 42 | }) 43 | }) 44 | } 45 | 46 | _addPermissionParams (params) { 47 | return { 48 | Action: 'lambda:InvokeFunction', 49 | FunctionName: this._functionName(params), 50 | Principal: 'events.amazonaws.com', 51 | SourceArn: params.RuleArn, 52 | StatementId: params.ScheduleName 53 | } 54 | } 55 | 56 | _addPermission (params) { 57 | return new Promise((resolve, reject) => { 58 | const _params = this._addPermissionParams(params) 59 | this.lambda.addPermission(_params, (err, data) => { 60 | if (err) { 61 | if (err.code !== 'ResourceConflictException') reject(err) 62 | // If it exists it will result in an error but there is no problem. 63 | resolve('Permission already set') 64 | } 65 | resolve(data) 66 | }) 67 | }) 68 | } 69 | 70 | _putTargetsParams (params) { 71 | return { 72 | Rule: params.ScheduleName, 73 | Targets: [{ 74 | Arn: params.FunctionArn, 75 | Id: this._functionName(params), 76 | Input: params.Input != null ? JSON.stringify(params.Input) : '' 77 | }] 78 | } 79 | } 80 | 81 | _putTargets (params) { 82 | return new Promise((resolve, reject) => { 83 | const _params = this._putTargetsParams(params) 84 | this.cloudwatchevents.putTargets(_params, (err, data) => { 85 | // even if it is already registered, it will not be an error. 86 | if (err) reject(err) 87 | resolve(data) 88 | }) 89 | }) 90 | } 91 | 92 | add (params) { 93 | return Promise.resolve().then(() => { 94 | return this._putRule(params) 95 | }).then(rule => { 96 | return this._addPermission(Object.assign(params, rule)) 97 | }).then(data => { 98 | return this._putTargets(params) 99 | }) 100 | } 101 | } 102 | 103 | module.exports = ScheduleEvents 104 | -------------------------------------------------------------------------------- /node-lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motdotla/node-lambda/7fb81404d1f1c55e7f79cb9699ecff7df4b9284a/node-lambda.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-lambda", 3 | "version": "1.3.0", 4 | "description": "Command line tool for locally running and remotely deploying your node.js applications to Amazon Lambda.", 5 | "main": "lib/main.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "lint": "standard && standard bin/node-lambda", 11 | "test": "npm run lint && npm run unit", 12 | "unit": "mocha" 13 | }, 14 | "bin": { 15 | "node-lambda": "./bin/node-lambda" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/motdotla/node-lambda.git" 20 | }, 21 | "keywords": [ 22 | "lambda", 23 | "aws", 24 | "amazon", 25 | "amazon-lambda", 26 | "aws-lambda", 27 | "lambda-node", 28 | "deploy", 29 | "deployment" 30 | ], 31 | "readmeFilename": "README.md", 32 | "author": "motdotla", 33 | "license": "BSD-2-Clause", 34 | "engines": { 35 | "node": ">= 18.0.0" 36 | }, 37 | "dependencies": { 38 | "@dotenvx/dotenvx": "^1.14.0", 39 | "archiver": "^7.0.0", 40 | "aws-sdk": "^2.1377.0", 41 | "aws-xray-sdk-core": "^3.5.0", 42 | "commander": "^14.0.0", 43 | "continuation-local-storage": "^3.2.1", 44 | "fs-extra": "^11.1.1", 45 | "klaw": "^4.1.0", 46 | "minimatch": "^10.0.1", 47 | "proxy-agent": "^6.2.0" 48 | }, 49 | "devDependencies": { 50 | "aws-sdk-mock": "^6.0.4", 51 | "chai": "^5.0.0", 52 | "mocha": "^11.0.1", 53 | "node-zip": "^1.1.1", 54 | "standard": "^17.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x248c5A7855A8861e3277341A1285c5D7dE64ba73' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /test/cloudwatch_logs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let assert 4 | import('chai').then(chai => { 5 | assert = chai.assert 6 | }) 7 | const path = require('path') 8 | const aws = require('aws-sdk-mock') 9 | aws.setSDK(path.resolve('node_modules/aws-sdk')) 10 | const CloudWatchLogs = require(path.join('..', 'lib', 'cloudwatch_logs')) 11 | 12 | const mockResponse = { 13 | createLogGroup: { 14 | testCreateLogGroupResponse: 'An empty object is returned in the actual API' 15 | }, 16 | 17 | putRetentionPolicy: { 18 | testPutRetentionPolicyResponse: 'An empty object is returned in the actual API' 19 | } 20 | } 21 | 22 | const params = { 23 | FunctionName: 'node-lambda-test-function', 24 | retentionInDays: 14 25 | } 26 | 27 | let logs = null 28 | 29 | /* global before, after, describe, it */ 30 | describe('lib/cloudwatch_logs', () => { 31 | before(() => { 32 | aws.mock('CloudWatchLogs', 'createLogGroup', (params, callback) => { 33 | callback(null, mockResponse.createLogGroup) 34 | }) 35 | aws.mock('CloudWatchLogs', 'putRetentionPolicy', (params, callback) => { 36 | callback(null, mockResponse.putRetentionPolicy) 37 | }) 38 | 39 | logs = new CloudWatchLogs(require('aws-sdk')) 40 | }) 41 | 42 | after(() => aws.restore('CloudWatchLogs')) 43 | 44 | describe('_logGroupName', () => { 45 | it('correct value', () => { 46 | assert.equal( 47 | logs._logGroupName(params), 48 | '/aws/lambda/node-lambda-test-function' 49 | ) 50 | }) 51 | }) 52 | 53 | describe('_createLogGroup', () => { 54 | it('using mock', () => { 55 | return logs._createLogGroup(params).then((data) => { 56 | assert.deepEqual(data, mockResponse.createLogGroup) 57 | }) 58 | }) 59 | }) 60 | 61 | describe('_putRetentionPolicy', () => { 62 | it('using mock', () => { 63 | return logs._putRetentionPolicy(params).then((data) => { 64 | assert.deepEqual(data, mockResponse.putRetentionPolicy) 65 | }) 66 | }) 67 | }) 68 | 69 | describe('setLogsRetentionPolicy', () => { 70 | it('using mock', () => { 71 | return logs.setLogsRetentionPolicy(params).then((data) => { 72 | assert.deepEqual(data, mockResponse.putRetentionPolicy) 73 | }) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/handler/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // For testing AWSXRay support 4 | const AWSXRay = require('aws-xray-sdk-core') 5 | 6 | exports.handler = (event, context, callback) => { 7 | // It changes to a boolean value with `!!` 8 | context.callbackWaitsForEmptyEventLoop = 9 | !!event.callbackWaitsForEmptyEventLoop 10 | 11 | if (event.asyncTest) { 12 | setTimeout(() => console.log('sleep 3500 msec'), 3500) 13 | } 14 | 15 | // https://docs.aws.amazon.com/xray/latest/devguide/scorekeep-lambda.html 16 | // For testing AWSXRay support 17 | AWSXRay.captureFunc('annotations', (subsegment) => { 18 | subsegment.addAnnotation('Name', 'name') 19 | subsegment.addAnnotation('UserID', 'piyo') 20 | }) 21 | 22 | /* eslint-disable no-eval */ 23 | eval(event.callbackCode) 24 | } 25 | -------------------------------------------------------------------------------- /test/node-lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let assert 4 | import('chai').then(chai => { 5 | assert = chai.assert 6 | }) 7 | const process = require('process') 8 | const path = require('path') 9 | const fs = require('fs-extra') 10 | const spawn = require('child_process').spawn 11 | const execSync = require('child_process').execSync 12 | const nodeLambdaPath = path.join(__dirname, '..', 'bin', 'node-lambda') 13 | 14 | const removeDotenvLog = (str) => { 15 | // eslint-disable-next-line no-control-regex 16 | return str.replace(/^\u001b\[38;5;142m\[dotenvx@.+\]injectingenv\(.+\)from.env\u001b\[39m/, '') 17 | } 18 | 19 | /* global before, after, describe, it */ 20 | // The reason for specifying the node command in this test is to support Windows. 21 | describe('bin/node-lambda', () => { 22 | describe('node-lambda run', () => { 23 | const _testMain = (expectedValues, done) => { 24 | const run = spawn('node', [ 25 | nodeLambdaPath, 'run', 26 | '--handler', '__test.handler', 27 | '--eventFile', 'event.json' 28 | ]) 29 | let stdoutString = '' 30 | let stderrString = '' 31 | run.stdout.on('data', (data) => { 32 | stdoutString += data.toString().replace(/\r|\n/g, '') 33 | }) 34 | run.stderr.on('data', (data) => { 35 | stderrString += data.toString().replace(/\r|\n/g, '') 36 | }) 37 | 38 | run.on('exit', (code) => { 39 | if (expectedValues.stdoutRegExp) { 40 | assert.match(stdoutString, expectedValues.stdoutRegExp) 41 | } 42 | if (expectedValues.stderrRegExp) { 43 | assert.match(stderrString, expectedValues.stderrRegExp) 44 | } 45 | assert.equal(code, expectedValues.exitCode) 46 | done() 47 | }) 48 | } 49 | 50 | const _generateEventFile = (eventObj) => { 51 | fs.writeFileSync('event.json', JSON.stringify(eventObj)) 52 | } 53 | 54 | before(() => { 55 | execSync(`node ${nodeLambdaPath} setup`) 56 | fs.copy(path.join(__dirname, 'handler', 'index.js'), '__test.js') 57 | }) 58 | 59 | after(() => { 60 | [ 61 | '.env', 62 | 'context.json', 63 | 'event.json', 64 | 'deploy.env', 65 | 'event_sources.json', 66 | '__test.js' 67 | ].forEach((file) => fs.unlinkSync(file)) 68 | }) 69 | 70 | describe('node-lambda run (Handler only sync processing)', () => { 71 | const eventObj = { 72 | asyncTest: false, 73 | callbackWaitsForEmptyEventLoop: true // True is the default value of Lambda 74 | } 75 | 76 | it('`node-lambda run` exitCode is `0` (callback(null))', (done) => { 77 | _generateEventFile(Object.assign(eventObj, { 78 | callbackCode: 'callback(null);' 79 | })) 80 | _testMain({ stdoutRegExp: /Success:$/, exitCode: 0 }, done) 81 | }) 82 | 83 | it('`node-lambda run` exitCode is `0` (callback(null, "text"))', (done) => { 84 | _generateEventFile(Object.assign(eventObj, { 85 | callbackCode: 'callback(null, "text");' 86 | })) 87 | _testMain({ stdoutRegExp: /Success:"text"$/, exitCode: 0 }, done) 88 | }) 89 | 90 | it('`node-lambda run` exitCode is `255` (callback(new Error("e")))', (done) => { 91 | _generateEventFile(Object.assign(eventObj, { 92 | callbackCode: 'callback(new Error("e"));' 93 | })) 94 | _testMain({ stdoutRegExp: /Error: Error: e$/, exitCode: 255 }, done) 95 | }) 96 | }) 97 | 98 | describe('node-lambda run (Handler includes async processing)', () => { 99 | describe('callbackWaitsForEmptyEventLoop = true', function () { 100 | this.timeout(5000) // give it time to setTimeout 101 | 102 | const eventObj = { 103 | asyncTest: true, 104 | callbackWaitsForEmptyEventLoop: true 105 | } 106 | 107 | it('`node-lambda run` exitCode is `0` (callback(null))', (done) => { 108 | _generateEventFile(Object.assign(eventObj, { 109 | callbackCode: 'callback(null);' 110 | })) 111 | _testMain({ stdoutRegExp: /Success:sleep 3500 msec$/, exitCode: 0 }, done) 112 | }) 113 | 114 | it('`node-lambda run` exitCode is `0` (callback(null, "text"))', (done) => { 115 | _generateEventFile(Object.assign(eventObj, { 116 | callbackCode: 'callback(null, "text");' 117 | })) 118 | _testMain({ stdoutRegExp: /Success:"text"sleep 3500 msec$/, exitCode: 0 }, done) 119 | }) 120 | 121 | it('`node-lambda run` exitCode is `255` (callback(new Error("e")))', (done) => { 122 | _generateEventFile(Object.assign(eventObj, { 123 | callbackCode: 'callback(new Error("e"));' 124 | })) 125 | _testMain({ stdoutRegExp: /Error: Error: esleep 3500 msec$/, exitCode: 255 }, done) 126 | }) 127 | }) 128 | 129 | describe('callbackWaitsForEmptyEventLoop = false', () => { 130 | const eventObj = { 131 | asyncTest: true, 132 | callbackWaitsForEmptyEventLoop: false 133 | } 134 | 135 | it('`node-lambda run` exitCode is `0` (callback(null))', (done) => { 136 | _generateEventFile(Object.assign(eventObj, { 137 | callbackCode: 'callback(null);' 138 | })) 139 | _testMain({ stdoutRegExp: /Success:$/, exitCode: 0 }, done) 140 | }) 141 | 142 | it('`node-lambda run` exitCode is `0` (callback(null, "text"))', (done) => { 143 | _generateEventFile(Object.assign(eventObj, { 144 | callbackCode: 'callback(null, "text");' 145 | })) 146 | _testMain({ stdoutRegExp: /Success:"text"$/, exitCode: 0 }, done) 147 | }) 148 | 149 | it('`node-lambda run` exitCode is `255` (callback(new Error("e")))', (done) => { 150 | _generateEventFile(Object.assign(eventObj, { 151 | callbackCode: 'callback(new Error("e"));' 152 | })) 153 | _testMain({ stdoutRegExp: /Error: Error: e$/, exitCode: 255 }, done) 154 | }) 155 | }) 156 | }) 157 | 158 | describe('node-lambda run (Runtime is not supported)', () => { 159 | const eventObj = { 160 | asyncTest: false, 161 | callbackWaitsForEmptyEventLoop: true // True is the default value of Lambda 162 | } 163 | 164 | before(() => { 165 | process.env.AWS_RUNTIME = 'test' 166 | }) 167 | after(() => { 168 | process.env.AWS_RUNTIME = 'nodejs20.x' 169 | }) 170 | 171 | it('`node-lambda run` exitCode is `254` (callback(null))', (done) => { 172 | _generateEventFile(Object.assign(eventObj, { 173 | callbackCode: 'callback(null);' 174 | })) 175 | _testMain({ 176 | stderrRegExp: /^Runtime \[test\] is not supported\.$/, 177 | exitCode: 254 178 | }, done) 179 | }) 180 | }) 181 | 182 | describe('node-lambda run (Runtime nodejs20.x is supported)', () => { 183 | const eventObj = { 184 | asyncTest: false, 185 | callbackWaitsForEmptyEventLoop: true // True is the default value of Lambda 186 | } 187 | 188 | before(() => { 189 | process.env.AWS_RUNTIME = 'nodejs20.x' 190 | }) 191 | 192 | it('`node-lambda run` exitCode is not `254` (callback(null))', (done) => { 193 | _generateEventFile(Object.assign(eventObj, { 194 | callbackCode: 'callback(null);' 195 | })) 196 | _testMain({ stdoutRegExp: /Success:$/, exitCode: 0 }, done) 197 | }) 198 | }) 199 | 200 | describe('node-lambda run (Multiple events)', () => { 201 | const eventObj = [{ 202 | asyncTest: false, 203 | callbackWaitsForEmptyEventLoop: true, 204 | callbackCode: 'callback(null);', 205 | no: 1 206 | }, { 207 | asyncTest: false, 208 | callbackWaitsForEmptyEventLoop: true, 209 | callbackCode: 'callback(null);', 210 | no: 2 211 | }, { 212 | asyncTest: false, 213 | callbackWaitsForEmptyEventLoop: true, 214 | callbackCode: 'callback(null);', 215 | no: 3 216 | }] 217 | 218 | it('`node-lambda run` exitCode is `0`', function (done) { 219 | this.timeout(10000) // give it time to multiple executions 220 | _generateEventFile(eventObj) 221 | _testMain({ 222 | stdoutRegExp: /no: 1.+no: 2.+no: 3.+Success:/, 223 | exitCode: 0 224 | }, done) 225 | }) 226 | }) 227 | 228 | describe('node-lambda run (disable Multiple events)', () => { 229 | const eventObj = [{ 230 | asyncTest: false, 231 | callbackWaitsForEmptyEventLoop: true, 232 | callbackCode: 'callback(null);', 233 | no: 1 234 | }, { 235 | asyncTest: false, 236 | callbackWaitsForEmptyEventLoop: true, 237 | callbackCode: 'callback(null);', 238 | no: 2 239 | }, { 240 | asyncTest: false, 241 | callbackWaitsForEmptyEventLoop: true, 242 | callbackCode: 'callback(null);', 243 | no: 3 244 | }] 245 | 246 | it('`node-lambda run` exitCode is `0`', function (done) { 247 | this.timeout(10000) // give it time to multiple executions 248 | 249 | _generateEventFile(eventObj) 250 | const run = spawn('node', [ 251 | nodeLambdaPath, 'run', 252 | '--handler', 'index.handler', 253 | '--eventFile', 'event.json', 254 | '-M', 'false' 255 | ]) 256 | let stdoutString = '' 257 | run.stdout.on('data', (data) => { 258 | stdoutString += data.toString().replace(/\r|\n|\s/g, '') 259 | }) 260 | 261 | run.on('exit', (code) => { 262 | const expected = 'Runningindex.handler==================================event' + 263 | '[{asyncTest:false,callbackWaitsForEmptyEventLoop:true,callbackCode:\'callback(null);\',no:1},' + 264 | '{asyncTest:false,callbackWaitsForEmptyEventLoop:true,callbackCode:\'callback(null);\',no:2},' + 265 | '{asyncTest:false,callbackWaitsForEmptyEventLoop:true,callbackCode:\'callback(null);\',no:3}]' + 266 | '==================================Stoppingindex.handlerSuccess:' 267 | 268 | assert.equal(removeDotenvLog(stdoutString), expected) 269 | assert.equal(code, 0) 270 | done() 271 | }) 272 | }) 273 | }) 274 | 275 | describe('node-lambda run (API Gateway events)', () => { 276 | const eventObj = { 277 | asyncTest: false, 278 | callbackWaitsForEmptyEventLoop: true, 279 | callbackCode: 'callback(null);' 280 | } 281 | 282 | it('`node-lambda run` exitCode is `0`', function (done) { 283 | _generateEventFile(eventObj) 284 | const run = spawn('node', [ 285 | nodeLambdaPath, 'run', 286 | '--handler', 'index.handler', 287 | '--eventFile', 'event.json', 288 | '--apiGateway' 289 | ]) 290 | let stdoutString = '' 291 | run.stdout.on('data', (data) => { 292 | stdoutString += data.toString().replace(/\r|\n|\s/g, '') 293 | }) 294 | 295 | run.on('exit', (code) => { 296 | const expected = '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!EmulateonlythebodyoftheAPIGatewayevent.' + 297 | '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!Runningindex.handler==================================event' + 298 | '{body:\'{"asyncTest":false,"callbackWaitsForEmptyEventLoop":true,"callbackCode":"callback(null);"}\'}' + 299 | '==================================Stoppingindex.handlerSuccess:' 300 | assert.equal(removeDotenvLog(stdoutString), expected) 301 | assert.equal(code, 0) 302 | done() 303 | }) 304 | }) 305 | }) 306 | 307 | describe('node-lambda run (by *.mjs)', () => { 308 | it('`node-lambda run` by index.mjs', function (done) { 309 | const run = spawn('node', [ 310 | nodeLambdaPath, 'run', 311 | '--handler', 'index_mjs.handler' 312 | ]) 313 | let stdoutString = '' 314 | run.stdout.on('data', (data) => { 315 | stdoutString += data.toString().replace(/\r|\n|\s/g, '') 316 | }) 317 | 318 | run.on('exit', (code) => { 319 | const expected = 'Runningindex.handler(mjs)' + 320 | '==================================' + 321 | 'event{asyncTest:false,callbackWaitsForEmptyEventLoop:true,callbackCode:\'callback(null);\'}' + 322 | '==================================' + 323 | 'Stoppingindex.handler(mjs)Success:' 324 | assert.equal(removeDotenvLog(stdoutString), expected) 325 | assert.equal(code, 0) 326 | done() 327 | }) 328 | }) 329 | }) 330 | 331 | describe('node-lambda run (handler file not found)', () => { 332 | it('`node-lambda run` Invalid handler specification.', function (done) { 333 | const run = spawn('node', [ 334 | nodeLambdaPath, 'run', 335 | '--handler', 'not_found.handler' 336 | ]) 337 | let stdoutString = '' 338 | run.stdout.on('data', (data) => { 339 | stdoutString += data.toString().replace(/\r|\n|\s/g, '') 340 | }) 341 | let stderrString = '' 342 | run.stderr.on('data', (data) => { 343 | stderrString += data.toString() 344 | }) 345 | 346 | run.on('exit', (code) => { 347 | assert.equal(removeDotenvLog(stdoutString), '') 348 | assert.match(stderrString, /Handler file not found\./) 349 | assert.equal(code, 255) 350 | done() 351 | }) 352 | }) 353 | }) 354 | }) 355 | 356 | describe('node-lambda duplicate check of short option', () => { 357 | const duplicateCheckTestFunc = (type, done) => { 358 | const cmd = spawn('node', [nodeLambdaPath, type, '-h']) 359 | let stdoutString = '' 360 | cmd.stdout.on('data', (data) => { 361 | stdoutString += data.toString() 362 | }) 363 | 364 | cmd.on('exit', (code) => { 365 | assert.equal(code, 0) 366 | 367 | const shortOptions = stdoutString.split('\n').filter(line => { 368 | return line.match(/^\s+-/) 369 | }).map(line => { 370 | return line.split(/\s+/)[1] 371 | }) 372 | const uniqueShortOptions = shortOptions.filter((option, index, array) => { 373 | return array.indexOf(option) === index 374 | }) 375 | assert.equal(shortOptions.length, uniqueShortOptions.length) 376 | done() 377 | }) 378 | } 379 | 380 | ['deploy', 'run', 'setup'].forEach(type => { 381 | it(`cmd:${type}`, (done) => { 382 | duplicateCheckTestFunc(type, done) 383 | }) 384 | }) 385 | }) 386 | 387 | describe('node-lambda --version', () => { 388 | const packageJson = require(path.join(__dirname, '..', 'package.json')) 389 | it('The current version is displayed', () => { 390 | const ret = execSync(`node ${nodeLambdaPath} --version`) 391 | assert.equal( 392 | ret.toString().trim().split('\n').pop(), 393 | packageJson.version 394 | ) 395 | }) 396 | }) 397 | }) 398 | -------------------------------------------------------------------------------- /test/post_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | printf "Your environment is $1" 3 | -------------------------------------------------------------------------------- /test/s3_deploy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let assert 4 | import('chai').then(chai => { 5 | assert = chai.assert 6 | }) 7 | const process = require('process') 8 | const path = require('path') 9 | const aws = require('aws-sdk-mock') 10 | aws.setSDK(path.resolve('node_modules/aws-sdk')) 11 | const S3Deploy = require('../lib/s3_deploy') 12 | 13 | const mockResponse = { 14 | createBucket: { Location: 'createBucket' }, 15 | putObject: { ETag: 'putObject' } 16 | } 17 | 18 | let s3Deploy = null 19 | 20 | /* global describe, it, before, after */ 21 | describe('lib/s3_deploy', () => { 22 | before(() => { 23 | aws.mock('S3', 'putObject', (params, callback) => { 24 | callback(null, mockResponse.putObject) 25 | }) 26 | aws.mock('S3', 'createBucket', (params, callback) => { 27 | callback(null, mockResponse.createBucket) 28 | }) 29 | 30 | s3Deploy = new S3Deploy(require('aws-sdk')) 31 | }) 32 | 33 | after(() => { 34 | aws.restore('S3') 35 | }) 36 | 37 | describe('_md5', () => { 38 | it('md5("hoge") === "ea703e7aa1efda0064eaa507d9e8ab7e"', () => { 39 | assert.equal(s3Deploy._md5('hoge'), 'ea703e7aa1efda0064eaa507d9e8ab7e') 40 | }) 41 | }) 42 | 43 | describe('_convertRegionStringToEnvVarName', () => { 44 | it('Upper case. Replace "-" with "_".', () => { 45 | [{ 46 | value: 'us-west-1', 47 | expected: 'US_WEST_1' 48 | }, { 49 | value: 'ap-southeast-2', 50 | expected: 'AP_SOUTHEAST_2' 51 | }].forEach((test) => { 52 | assert.equal( 53 | s3Deploy._convertRegionStringToEnvVarName(test.value), 54 | test.expected, 55 | test 56 | ) 57 | }) 58 | }) 59 | }) 60 | 61 | describe('_getBucketNameFromEnvVar', () => { 62 | after(() => { 63 | delete process.env.S3_US_WEST_1_BUCKET 64 | }) 65 | 66 | it('is undefined', () => { 67 | assert.isUndefined(s3Deploy._getBucketNameFromEnvVar('us-west-1')) 68 | }) 69 | 70 | it('Get values from environment variables', () => { 71 | process.env.S3_US_WEST_1_BUCKET = 'bucketName' 72 | assert.equal( 73 | s3Deploy._getBucketNameFromEnvVar('us-west-1'), 74 | 'bucketName' 75 | ) 76 | }) 77 | }) 78 | 79 | describe('_getS3KeyPrefixFromEnvVar', () => { 80 | after(() => { 81 | delete process.env.S3_US_WEST_1_PREFIX 82 | }) 83 | 84 | it('is undefined', () => { 85 | assert.isUndefined(s3Deploy._getS3KeyPrefixFromEnvVar('us-west-1')) 86 | }) 87 | 88 | it('Get values from environment variables', () => { 89 | process.env.S3_US_WEST_1_PREFIX = 's3KeyPrefix' 90 | assert.equal( 91 | s3Deploy._getS3KeyPrefixFromEnvVar('us-west-1'), 92 | 's3KeyPrefix' 93 | ) 94 | }) 95 | }) 96 | 97 | describe('_bucketName', () => { 98 | after(() => { 99 | delete process.env.S3_TEST_REGION_BUCKET 100 | }) 101 | 102 | it('FunctionName + region + md5()', () => { 103 | const params = { 104 | FunctionName: 'node-lambda-name', 105 | region: 'test_region' 106 | } 107 | assert.equal( 108 | s3Deploy._bucketName(params), 109 | 'node-lambda-name-test_region-aac849d59d2be828b793609e03d8241d' 110 | ) 111 | }) 112 | 113 | it('Use environment variables', () => { 114 | process.env.S3_TEST_REGION_BUCKET = 's3-test-region-bucket' 115 | const params = { 116 | FunctionName: 'node-lambda-name', 117 | region: 'test_region' 118 | } 119 | assert.equal(s3Deploy._bucketName(params), 's3-test-region-bucket') 120 | }) 121 | }) 122 | 123 | describe('_s3Key', () => { 124 | after(() => { 125 | delete process.env.S3_TEST_REGION_PREFIX 126 | }) 127 | 128 | it('"deploy-package" + FunctionName + ".zip"', () => { 129 | const params = { 130 | FunctionName: 'node-lambda-name', 131 | region: 'test_region' 132 | } 133 | assert.equal( 134 | s3Deploy._s3Key(params), 135 | 'deploy-package-node-lambda-name.zip' 136 | ) 137 | }) 138 | 139 | it('Use environment variables', () => { 140 | process.env.S3_TEST_REGION_PREFIX = 's3-test-region-prefix/' 141 | const params = { 142 | FunctionName: 'node-lambda-name', 143 | region: 'test_region' 144 | } 145 | assert.equal( 146 | s3Deploy._s3Key(params), 147 | 's3-test-region-prefix/deploy-package-node-lambda-name.zip' 148 | ) 149 | }) 150 | }) 151 | 152 | describe('_getS3Location', () => { 153 | it('is null', () => { 154 | assert.isNull(s3Deploy._getS3Location('hoge')) 155 | }) 156 | 157 | it('=== "ap-southeast-1"', () => { 158 | assert.equal(s3Deploy._getS3Location('ap-southeast-1'), 'ap-southeast-1') 159 | }) 160 | }) 161 | 162 | describe('_createBucket', () => { 163 | it('using mock', () => { 164 | const params = { 165 | bucketName: 'node-lambda-test-bucket', 166 | region: 'ap-southeast-1' 167 | } 168 | return s3Deploy._createBucket(params).then((result) => { 169 | assert.deepEqual(result, mockResponse.createBucket) 170 | }) 171 | }) 172 | }) 173 | 174 | describe('_putObject', () => { 175 | it('using mock', () => { 176 | const params = { 177 | bucketName: 'node-lambda-test-bucket', 178 | s3Key: 'testKey' 179 | } 180 | return s3Deploy._putObject(params, 'buffer').then((result) => { 181 | assert.deepEqual(result, mockResponse.putObject) 182 | }) 183 | }) 184 | }) 185 | 186 | describe('putPackage', () => { 187 | it('using mock', () => { 188 | const params = { FunctionName: 'node-lambda-test-bucket-20180801' } 189 | return s3Deploy.putPackage(params, 'ap-southeast-1', 'buffer').then((result) => { 190 | assert.deepEqual(result, { 191 | S3Bucket: 'node-lambda-test-bucket-20180801-ap-southeast-1-6c696118a497125', 192 | S3Key: 'deploy-package-node-lambda-test-bucket-20180801.zip' 193 | }) 194 | }) 195 | }) 196 | }) 197 | }) 198 | -------------------------------------------------------------------------------- /test/s3_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let assert 4 | import('chai').then(chai => { 5 | assert = chai.assert 6 | }) 7 | const path = require('path') 8 | const aws = require('aws-sdk-mock') 9 | aws.setSDK(path.resolve('node_modules/aws-sdk')) 10 | const S3Events = require('../lib/s3_events') 11 | 12 | const params = { 13 | FunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function', 14 | Bucket: 'node-lambda-test-bucket', 15 | Events: ['s3:ObjectCreated:*'], 16 | Filter: null 17 | } 18 | 19 | const mockResponse = { 20 | addPermission: { 21 | Statement: JSON.stringify({ 22 | Sid: 'node-lambda-test-bucket', 23 | Resource: 'arn:aws:lambda:node-lambda-test-function', 24 | Effect: 'Allow', 25 | Principal: { Service: 's3.amazonaws.com' }, 26 | Action: ['lambda:InvokeFunction'], 27 | Condition: { ArnLike: { 'AWS:SourceArn': 'arn:aws:s3:::node-lambda-test-bucket' } } 28 | }) 29 | }, 30 | 31 | putBucketNotificationConfiguration: {} 32 | } 33 | 34 | let s3Events = null 35 | 36 | /* global before, after, describe, it */ 37 | describe('lib/s3_events', () => { 38 | before(() => { 39 | aws.mock('Lambda', 'addPermission', (params, callback) => { 40 | callback(null, mockResponse.addPermission) 41 | }) 42 | aws.mock('S3', 'putBucketNotificationConfiguration', (params, callback) => { 43 | callback(null, mockResponse.putBucketNotificationConfiguration) 44 | }) 45 | 46 | s3Events = new S3Events(require('aws-sdk')) 47 | }) 48 | 49 | after(() => { 50 | aws.restore('Lambda') 51 | aws.restore('S3') 52 | }) 53 | 54 | describe('_functionName', () => { 55 | it('Extract name from FunctionArn', () => { 56 | assert.equal( 57 | s3Events._functionName(params), 58 | 'node-lambda-test-function' 59 | ) 60 | }) 61 | }) 62 | 63 | describe('_statementId', () => { 64 | it('StatementId that matches /[a-zA-Z0-9-_]+/.', () => { 65 | [{ 66 | params, 67 | expected: 'node-lambda-test-bucket' 68 | }, { 69 | params: { Bucket: 'example.com' }, 70 | expected: 'example_com' 71 | }].forEach((test) => { 72 | const actual = s3Events._statementId(test.params) 73 | assert.equal(actual, test.expected, test) 74 | assert.match(actual, /[a-zA-Z0-9-_]+/, test) 75 | }) 76 | }) 77 | }) 78 | 79 | describe('_addPermissionParams', () => { 80 | it('Return parameters for lambda.addPermission()', () => { 81 | const expected = { 82 | Action: 'lambda:InvokeFunction', 83 | FunctionName: 'node-lambda-test-function', 84 | Principal: 's3.amazonaws.com', 85 | SourceArn: 'arn:aws:s3:::node-lambda-test-bucket', 86 | StatementId: 'node-lambda-test-bucket' 87 | } 88 | assert.deepEqual(s3Events._addPermissionParams(params), expected) 89 | }) 90 | }) 91 | 92 | describe('_lambdaFunctionConfiguration', () => { 93 | it('Return parameters for s3._lambdaFunctionConfiguration(). No Filter', () => { 94 | const expected = { 95 | Events: ['s3:ObjectCreated:*'], 96 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function' 97 | } 98 | assert.deepEqual( 99 | s3Events._lambdaFunctionConfiguration(params), 100 | expected 101 | ) 102 | }) 103 | 104 | it('Return parameters for s3.putBucketNotificationConfiguration(). Use Filter', () => { 105 | const expected = { 106 | Events: ['s3:ObjectCreated:*'], 107 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function', 108 | Filter: { 109 | Key: { 110 | FilterRules: [{ 111 | Name: 'prefix', 112 | Value: 'test-prefix' 113 | }] 114 | } 115 | } 116 | } 117 | const _params = Object.assign({}, params) 118 | _params.Filter = { 119 | Key: { 120 | FilterRules: [{ 121 | Name: 'prefix', 122 | Value: 'test-prefix' 123 | }] 124 | } 125 | } 126 | assert.deepEqual( 127 | s3Events._lambdaFunctionConfiguration(_params), 128 | expected 129 | ) 130 | }) 131 | }) 132 | 133 | describe('_paramsListToBucketNotificationConfigurations', () => { 134 | describe('The number of elements of paramsList is 1', () => { 135 | it('Return parameter list of putBucketNotificationConfiguration', () => { 136 | const expected = [{ 137 | Bucket: 'node-lambda-test-bucket', 138 | NotificationConfiguration: { 139 | LambdaFunctionConfigurations: [{ 140 | Events: ['s3:ObjectCreated:*'], 141 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function' 142 | }] 143 | } 144 | }] 145 | assert.deepEqual( 146 | s3Events._paramsListToBucketNotificationConfigurations([params]), 147 | expected 148 | ) 149 | }) 150 | }) 151 | describe('The number of elements of paramsList is 2. Same bucket', () => { 152 | it('Return parameter list of putBucketNotificationConfiguration', () => { 153 | const expected = [{ 154 | Bucket: 'node-lambda-test-bucket', 155 | NotificationConfiguration: { 156 | LambdaFunctionConfigurations: [{ 157 | Events: ['s3:ObjectCreated:*'], 158 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function' 159 | }, { 160 | Events: ['s3:ObjectDelete:*'], 161 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function' 162 | }] 163 | } 164 | }] 165 | 166 | const paramsDeleteEvent = Object.assign({}, params) 167 | paramsDeleteEvent.Events = ['s3:ObjectDelete:*'] 168 | assert.deepEqual( 169 | s3Events._paramsListToBucketNotificationConfigurations([ 170 | params, 171 | paramsDeleteEvent 172 | ]), 173 | expected 174 | ) 175 | }) 176 | }) 177 | describe('The number of elements of paramsList is 2. Different bucket', () => { 178 | it('Return parameter list of putBucketNotificationConfiguration', () => { 179 | const expected = [{ 180 | Bucket: 'node-lambda-test-bucket', 181 | NotificationConfiguration: { 182 | LambdaFunctionConfigurations: [{ 183 | Events: ['s3:ObjectCreated:*'], 184 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function' 185 | }] 186 | } 187 | }, { 188 | Bucket: 'node-lambda-test-bucket2', 189 | NotificationConfiguration: { 190 | LambdaFunctionConfigurations: [{ 191 | Events: ['s3:ObjectCreated:*'], 192 | LambdaFunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function' 193 | }] 194 | } 195 | }] 196 | 197 | const paramsDifferentBucket = Object.assign({}, params) 198 | paramsDifferentBucket.Bucket = 'node-lambda-test-bucket2' 199 | assert.deepEqual( 200 | s3Events._paramsListToBucketNotificationConfigurations([ 201 | params, 202 | paramsDifferentBucket 203 | ]), 204 | expected 205 | ) 206 | }) 207 | }) 208 | }) 209 | 210 | describe('_addPermission', () => { 211 | it('using mock', () => { 212 | return s3Events._addPermission(params).then(data => { 213 | assert.deepEqual(data, mockResponse.addPermission) 214 | }) 215 | }) 216 | }) 217 | 218 | describe('_putBucketNotificationConfiguration', () => { 219 | it('using mock', () => { 220 | const putBucketNotificationConfigurationParams = 221 | s3Events._paramsListToBucketNotificationConfigurations([params])[0] 222 | return s3Events._putBucketNotificationConfiguration(putBucketNotificationConfigurationParams).then(data => { 223 | assert.deepEqual(data, mockResponse.putBucketNotificationConfiguration) 224 | }) 225 | }) 226 | }) 227 | 228 | describe('add', () => { 229 | it('using mock', () => { 230 | return s3Events.add([params]).then(data => { 231 | assert.deepEqual(data, mockResponse.putBucketNotificationConfiguration) 232 | }) 233 | }) 234 | }) 235 | }) 236 | -------------------------------------------------------------------------------- /test/schedule_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let assert 4 | import('chai').then(chai => { 5 | assert = chai.assert 6 | }) 7 | const path = require('path') 8 | const aws = require('aws-sdk-mock') 9 | aws.setSDK(path.resolve('node_modules/aws-sdk')) 10 | const ScheduleEvents = require(path.join('..', 'lib', 'schedule_events')) 11 | 12 | const params = { 13 | FunctionArn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function', 14 | ScheduleName: 'node-lambda-test-schedule', 15 | ScheduleState: 'ENABLED', 16 | ScheduleExpression: 'rate(1 hour)', 17 | ScheduleDescription: null 18 | } 19 | 20 | const mockResponse = { 21 | putRule: { 22 | RuleArn: 'arn:aws:events:hoge:fuga' 23 | }, 24 | 25 | addPermission: { 26 | Statement: JSON.stringify({ 27 | Sid: 'node-lambda-test-schedule', 28 | Resource: 'arn:aws:lambda:piyo', 29 | Effect: 'Allow', 30 | Principal: { Service: 'events.amazonaws.com' }, 31 | Action: ['lambda:InvokeFunction'], 32 | Condition: { ArnLike: { 'AWS:SourceArn': 'arn:aws:events:hoge:fuga' } } 33 | }) 34 | }, 35 | 36 | putTargets: { 37 | FailedEntries: [], 38 | FailedEntryCount: 0 39 | } 40 | } 41 | 42 | let schedule = null 43 | 44 | /* global before, after, describe, it */ 45 | describe('lib/schedule_events', () => { 46 | before(() => { 47 | aws.mock('CloudWatchEvents', 'putRule', (params, callback) => { 48 | callback(null, mockResponse.putRule) 49 | }) 50 | aws.mock('CloudWatchEvents', 'putTargets', (params, callback) => { 51 | callback(null, mockResponse.putTargets) 52 | }) 53 | aws.mock('Lambda', 'addPermission', (params, callback) => { 54 | callback(null, mockResponse.addPermission) 55 | }) 56 | 57 | schedule = new ScheduleEvents(require('aws-sdk')) 58 | }) 59 | 60 | after(() => { 61 | aws.restore('CloudWatchEvents') 62 | aws.restore('Lambda') 63 | }) 64 | 65 | describe('_ruleDescription (default)', () => { 66 | it('correct value', () => { 67 | assert.equal( 68 | schedule._ruleDescription(params), 69 | 'node-lambda-test-schedule - rate(1 hour)' 70 | ) 71 | }) 72 | }) 73 | 74 | describe('_ruleDescription (custom)', () => { 75 | before(() => { 76 | params.ScheduleDescription = 'Run node-lambda-test-function once per hour' 77 | }) 78 | 79 | after(() => { 80 | params.ScheduleDescription = null 81 | }) 82 | 83 | it('correct value', () => { 84 | assert.equal( 85 | schedule._ruleDescription(params), 86 | 'Run node-lambda-test-function once per hour' 87 | ) 88 | }) 89 | }) 90 | 91 | describe('_functionName', () => { 92 | it('correct value', () => { 93 | assert.equal( 94 | schedule._functionName(params), 95 | 'node-lambda-test-function' 96 | ) 97 | }) 98 | }) 99 | 100 | describe('_putRulePrams', () => { 101 | it('correct value', () => { 102 | const expected = { 103 | Name: 'node-lambda-test-schedule', 104 | Description: 'node-lambda-test-schedule - rate(1 hour)', 105 | State: 'ENABLED', 106 | ScheduleExpression: 'rate(1 hour)' 107 | } 108 | assert.deepEqual(schedule._putRulePrams(params), expected) 109 | }) 110 | }) 111 | 112 | describe('_addPermissionParams', () => { 113 | it('correct value', () => { 114 | const expected = { 115 | Action: 'lambda:InvokeFunction', 116 | FunctionName: 'node-lambda-test-function', 117 | Principal: 'events.amazonaws.com', 118 | SourceArn: 'arn:aws:events:hoge:fuga', 119 | StatementId: 'node-lambda-test-schedule' 120 | } 121 | const _params = Object.assign(params, mockResponse.putRule) 122 | assert.deepEqual(schedule._addPermissionParams(_params), expected) 123 | }) 124 | }) 125 | 126 | describe('_putTargetsParams', () => { 127 | it('correct value (No "Input" setting)', () => { 128 | const expected = { 129 | Rule: 'node-lambda-test-schedule', 130 | Targets: [{ 131 | Arn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function', 132 | Id: 'node-lambda-test-function', 133 | Input: '' 134 | }] 135 | } 136 | assert.deepEqual(schedule._putTargetsParams(params), expected) 137 | }) 138 | 139 | it('correct value ("Input" setting)', () => { 140 | const expected = { 141 | Rule: 'node-lambda-test-schedule', 142 | Targets: [{ 143 | Arn: 'arn:aws:lambda:us-west-2:XXX:function:node-lambda-test-function', 144 | Id: 'node-lambda-test-function', 145 | Input: '{"key":"value"}' 146 | }] 147 | } 148 | assert.deepEqual( 149 | schedule._putTargetsParams(Object.assign({ Input: { key: 'value' } }, params)), 150 | expected 151 | ) 152 | }) 153 | }) 154 | 155 | describe('_putRule', () => { 156 | it('using mock', () => { 157 | return schedule._putRule(params).then((data) => { 158 | assert.deepEqual(data, mockResponse.putRule) 159 | }) 160 | }) 161 | }) 162 | 163 | describe('_addPermission', () => { 164 | it('using mock', () => { 165 | const _params = Object.assign(params, mockResponse.putTargets) 166 | return schedule._addPermission(_params).then((data) => { 167 | assert.deepEqual(data, mockResponse.addPermission) 168 | }) 169 | }) 170 | }) 171 | 172 | describe('_putTargets', () => { 173 | it('using mock', () => { 174 | return schedule._putTargets(params).then((data) => { 175 | assert.deepEqual(data, mockResponse.putTargets) 176 | }) 177 | }) 178 | }) 179 | 180 | describe('add', () => { 181 | it('using mock', () => { 182 | return schedule.add(params).then((data) => { 183 | assert.deepEqual(data, mockResponse.putTargets) 184 | }) 185 | }) 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /test/testPj/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motdotla/node-lambda/7fb81404d1f1c55e7f79cb9699ecff7df4b9284a/test/testPj/index.js -------------------------------------------------------------------------------- /test/testPj/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testcodedirectory", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "testcodedirectory", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "dotenv": "^16.3.1" 13 | }, 14 | "devDependencies": { 15 | "chai": "^4.3.7" 16 | } 17 | }, 18 | "node_modules/assertion-error": { 19 | "version": "1.1.0", 20 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 21 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 22 | "dev": true, 23 | "engines": { 24 | "node": "*" 25 | } 26 | }, 27 | "node_modules/chai": { 28 | "version": "4.3.7", 29 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", 30 | "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", 31 | "dev": true, 32 | "dependencies": { 33 | "assertion-error": "^1.1.0", 34 | "check-error": "^1.0.2", 35 | "deep-eql": "^4.1.2", 36 | "get-func-name": "^2.0.0", 37 | "loupe": "^2.3.1", 38 | "pathval": "^1.1.1", 39 | "type-detect": "^4.0.5" 40 | }, 41 | "engines": { 42 | "node": ">=4" 43 | } 44 | }, 45 | "node_modules/check-error": { 46 | "version": "1.0.2", 47 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 48 | "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", 49 | "dev": true, 50 | "engines": { 51 | "node": "*" 52 | } 53 | }, 54 | "node_modules/deep-eql": { 55 | "version": "4.1.3", 56 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", 57 | "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", 58 | "dev": true, 59 | "dependencies": { 60 | "type-detect": "^4.0.0" 61 | }, 62 | "engines": { 63 | "node": ">=6" 64 | } 65 | }, 66 | "node_modules/dotenv": { 67 | "version": "16.3.1", 68 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", 69 | "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", 70 | "engines": { 71 | "node": ">=12" 72 | }, 73 | "funding": { 74 | "url": "https://github.com/motdotla/dotenv?sponsor=1" 75 | } 76 | }, 77 | "node_modules/get-func-name": { 78 | "version": "2.0.0", 79 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 80 | "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", 81 | "dev": true, 82 | "engines": { 83 | "node": "*" 84 | } 85 | }, 86 | "node_modules/loupe": { 87 | "version": "2.3.6", 88 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", 89 | "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", 90 | "dev": true, 91 | "dependencies": { 92 | "get-func-name": "^2.0.0" 93 | } 94 | }, 95 | "node_modules/pathval": { 96 | "version": "1.1.1", 97 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 98 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 99 | "dev": true, 100 | "engines": { 101 | "node": "*" 102 | } 103 | }, 104 | "node_modules/type-detect": { 105 | "version": "4.0.8", 106 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 107 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 108 | "dev": true, 109 | "engines": { 110 | "node": ">=4" 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/testPj/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testPj", 3 | "dependencies": { 4 | "dotenv": "^16.3.1" 5 | }, 6 | "devDependencies": { 7 | "chai": "^4.3.7" 8 | } 9 | } 10 | --------------------------------------------------------------------------------